├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── .yo-rc.json ├── README.MD ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── lib │ ├── CanvasManager.ts │ ├── Particle.ts │ ├── ParticleInteraction.ts │ ├── ParticlesManager.ts │ ├── index.ts │ ├── interfaces.ts │ └── utils.ts ├── package.json ├── particles.component.ts ├── particles.directive.ts └── tsconfig.es5.json ├── tools └── gulp │ └── inline-resources.js ├── tsconfig.json └── tslint.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | 5 | # TypeScript 6 | src/*.js 7 | src/*.map 8 | src/*.d.ts 9 | 10 | # JetBrains 11 | .idea 12 | .project 13 | .settings 14 | .idea/* 15 | *.iml 16 | 17 | # VS Code 18 | .vscode/* 19 | .vs/ 20 | 21 | # Windows 22 | Thumbs.db 23 | Desktop.ini 24 | 25 | # Mac 26 | .DS_Store 27 | **/.DS_Store 28 | 29 | # Ngc generated files 30 | **/*.ngfactory.ts 31 | 32 | # Build files 33 | dist/* 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | docs/* 5 | # DO NOT IGNORE TYPESCRIPT FILES FOR NPM 6 | # TypeScript 7 | # *.js 8 | # *.map 9 | # *.d.ts 10 | 11 | # JetBrains 12 | .idea 13 | .project 14 | .settings 15 | .idea/* 16 | *.iml 17 | 18 | # VS Code 19 | .vscode/* 20 | 21 | # Windows 22 | Thumbs.db 23 | Desktop.ini 24 | 25 | # Mac 26 | .DS_Store 27 | **/.DS_Store 28 | 29 | # Ngc generated files 30 | **/*.ngfactory.ts 31 | 32 | # Library files 33 | src/* 34 | build/* 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '4.2.1' 5 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-angular2-library": { 3 | "promptValues": { 4 | "gitRepositoryUrl": "https://github.com/ryuKKu-/angular-particle" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # angular-particle 2 | 3 | Implementation of [particle.js](https://github.com/VincentGarreau/particles.js/) with TypeScript for Angular2/4. Inspired by [react-particles-js](https://github.com/Wufe/react-particles-js) 4 | 5 | ## Installation 6 | 7 | To install this library, run: 8 | 9 | ```bash 10 | $ npm install angular-particle --save 11 | ``` 12 | 13 | ## How to use 14 | 15 | ```typescript 16 | // Import ParticlesModule 17 | import { ParticlesModule } from 'angular-particle'; 18 | 19 | @NgModule({ 20 | declarations: [ 21 | ... 22 | ], 23 | imports: [ 24 | ... 25 | ParticlesModule 26 | ], 27 | providers: [], 28 | bootstrap: [] 29 | }) 30 | export class AppModule { } 31 | ``` 32 | 33 | And just use the component in your HTML 34 | 35 | ```html 36 | 37 | ``` 38 | 39 | Parameters configuration can be found [here](http://vincentgarreau.com/particles.js/). If you don't provide any parameters, default one are used. 40 | 41 | 42 | ## Properties 43 | 44 | | Property | Type | Definition | 45 | | -------- | ------ | --------------------------------------- | 46 | | params | object | The parameters for particle.js | 47 | | style | object | The style of the canvas container | 48 | | width | number | The width of the canvas element (in %) | 49 | | height | number | The height of the canvas element (in %) | 50 | 51 | 52 | ## Example 53 | 54 | ```typescript 55 | 56 | @Component({ 57 | selector: 'app-root', 58 | templateUrl: './app.component.html', 59 | styleUrls: ['./app.component.css'] 60 | }) 61 | export class AppComponent implements OnInit { 62 | myStyle: object = {}; 63 | myParams: object = {}; 64 | width: number = 100; 65 | height: number = 100; 66 | 67 | ngOnInit() { 68 | this.myStyle = { 69 | 'position': 'fixed', 70 | 'width': '100%', 71 | 'height': '100%', 72 | 'z-index': -1, 73 | 'top': 0, 74 | 'left': 0, 75 | 'right': 0, 76 | 'bottom': 0, 77 | }; 78 | 79 | this.myParams = { 80 | particles: { 81 | number: { 82 | value: 200, 83 | }, 84 | color: { 85 | value: '#ff0000' 86 | }, 87 | shape: { 88 | type: 'triangle', 89 | }, 90 | } 91 | }; 92 | } 93 | } 94 | ``` 95 | 96 | ```html 97 | 98 | ``` 99 | 100 | 101 | ## License 102 | 103 | MIT © [Luc Raymond](mailto:ryukku.raymond@gmail.com) 104 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var gulp = require('gulp'), 3 | path = require('path'), 4 | ngc = require('@angular/compiler-cli/src/main').main, 5 | rollup = require('gulp-rollup'), 6 | rename = require('gulp-rename'), 7 | del = require('del'), 8 | runSequence = require('run-sequence'), 9 | inlineResources = require('./tools/gulp/inline-resources'); 10 | 11 | const rootFolder = path.join(__dirname); 12 | const srcFolder = path.join(rootFolder, 'src'); 13 | const tmpFolder = path.join(rootFolder, '.tmp'); 14 | const buildFolder = path.join(rootFolder, 'build'); 15 | const distFolder = path.join(rootFolder, 'dist'); 16 | 17 | /** 18 | * 1. Delete /dist folder 19 | */ 20 | gulp.task('clean:dist', function () { 21 | 22 | // Delete contents but not dist folder to avoid broken npm links 23 | // when dist directory is removed while npm link references it. 24 | return deleteFolders([distFolder + '/**', '!' + distFolder]); 25 | }); 26 | 27 | /** 28 | * 2. Clone the /src folder into /.tmp. If an npm link inside /src has been made, 29 | * then it's likely that a node_modules folder exists. Ignore this folder 30 | * when copying to /.tmp. 31 | */ 32 | gulp.task('copy:source', function () { 33 | return gulp.src([`${srcFolder}/**/*`, `!${srcFolder}/node_modules`]) 34 | .pipe(gulp.dest(tmpFolder)); 35 | }); 36 | 37 | /** 38 | * 3. Inline template (.html) and style (.css) files into the the component .ts files. 39 | * We do this on the /.tmp folder to avoid editing the original /src files 40 | */ 41 | gulp.task('inline-resources', function () { 42 | return Promise.resolve() 43 | .then(() => inlineResources(tmpFolder)); 44 | }); 45 | 46 | 47 | /** 48 | * 4. Run the Angular compiler, ngc, on the /.tmp folder. This will output all 49 | * compiled modules to the /build folder. 50 | */ 51 | gulp.task('ngc', function () { 52 | return ngc({ 53 | project: `${tmpFolder}/tsconfig.es5.json` 54 | }) 55 | .then((exitCode) => { 56 | if (exitCode === 1) { 57 | // This error is caught in the 'compile' task by the runSequence method callback 58 | // so that when ngc fails to compile, the whole compile process stops running 59 | throw new Error('ngc compilation failed'); 60 | } 61 | }); 62 | }); 63 | 64 | /** 65 | * 5. Run rollup inside the /build folder to generate our Flat ES module and place the 66 | * generated file into the /dist folder 67 | */ 68 | gulp.task('rollup:fesm', function () { 69 | return gulp.src(`${buildFolder}/**/*.js`) 70 | // transform the files here. 71 | .pipe(rollup({ 72 | 73 | // Bundle's entry point 74 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#entry 75 | entry: `${buildFolder}/index.js`, 76 | 77 | // Allow mixing of hypothetical and actual files. "Actual" files can be files 78 | // accessed by Rollup or produced by plugins further down the chain. 79 | // This prevents errors like: 'path/file' does not exist in the hypothetical file system 80 | // when subdirectories are used in the `src` directory. 81 | allowRealFiles: true, 82 | 83 | // A list of IDs of modules that should remain external to the bundle 84 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#external 85 | external: [ 86 | '@angular/core', 87 | '@angular/common' 88 | ], 89 | 90 | // Format of generated bundle 91 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#format 92 | format: 'es' 93 | })) 94 | .pipe(gulp.dest(distFolder)); 95 | }); 96 | 97 | /** 98 | * 6. Run rollup inside the /build folder to generate our UMD module and place the 99 | * generated file into the /dist folder 100 | */ 101 | gulp.task('rollup:umd', function () { 102 | return gulp.src(`${buildFolder}/**/*.js`) 103 | // transform the files here. 104 | .pipe(rollup({ 105 | 106 | // Bundle's entry point 107 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#entry 108 | entry: `${buildFolder}/index.js`, 109 | 110 | // Allow mixing of hypothetical and actual files. "Actual" files can be files 111 | // accessed by Rollup or produced by plugins further down the chain. 112 | // This prevents errors like: 'path/file' does not exist in the hypothetical file system 113 | // when subdirectories are used in the `src` directory. 114 | allowRealFiles: true, 115 | 116 | // A list of IDs of modules that should remain external to the bundle 117 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#external 118 | external: [ 119 | '@angular/core', 120 | '@angular/common' 121 | ], 122 | 123 | // Format of generated bundle 124 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#format 125 | format: 'umd', 126 | 127 | // Export mode to use 128 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#exports 129 | exports: 'named', 130 | 131 | // The name to use for the module for UMD/IIFE bundles 132 | // (required for bundles with exports) 133 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#modulename 134 | moduleName: 'angular-particle', 135 | 136 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#globals 137 | globals: { 138 | typescript: 'ts' 139 | } 140 | 141 | })) 142 | .pipe(rename('angular-particle.umd.js')) 143 | .pipe(gulp.dest(distFolder)); 144 | }); 145 | 146 | /** 147 | * 7. Copy all the files from /build to /dist, except .js files. We ignore all .js from /build 148 | * because with don't need individual modules anymore, just the Flat ES module generated 149 | * on step 5. 150 | */ 151 | gulp.task('copy:build', function () { 152 | return gulp.src([`${buildFolder}/**/*`, `!${buildFolder}/**/*.js`]) 153 | .pipe(gulp.dest(distFolder)); 154 | }); 155 | 156 | /** 157 | * 8. Copy package.json from /src to /dist 158 | */ 159 | gulp.task('copy:manifest', function () { 160 | return gulp.src([`${srcFolder}/package.json`]) 161 | .pipe(gulp.dest(distFolder)); 162 | }); 163 | 164 | /** 165 | * 9. Copy README.md from / to /dist 166 | */ 167 | gulp.task('copy:readme', function () { 168 | return gulp.src([path.join(rootFolder, 'README.MD')]) 169 | .pipe(gulp.dest(distFolder)); 170 | }); 171 | 172 | /** 173 | * 10. Delete /.tmp folder 174 | */ 175 | gulp.task('clean:tmp', function () { 176 | return deleteFolders([tmpFolder]); 177 | }); 178 | 179 | /** 180 | * 11. Delete /build folder 181 | */ 182 | gulp.task('clean:build', function () { 183 | return deleteFolders([buildFolder]); 184 | }); 185 | 186 | gulp.task('compile', function () { 187 | runSequence( 188 | 'clean:dist', 189 | 'copy:source', 190 | 'inline-resources', 191 | 'ngc', 192 | 'rollup:fesm', 193 | 'rollup:umd', 194 | 'copy:build', 195 | 'copy:manifest', 196 | 'copy:readme', 197 | 'clean:build', 198 | 'clean:tmp', 199 | function (err) { 200 | if (err) { 201 | console.log('ERROR:', err.message); 202 | deleteFolders([distFolder, tmpFolder, buildFolder]); 203 | } else { 204 | console.log('Compilation finished succesfully'); 205 | } 206 | }); 207 | }); 208 | 209 | /** 210 | * Watch for any change in the /src folder and compile files 211 | */ 212 | gulp.task('watch', function () { 213 | gulp.watch(`${srcFolder}/**/*`, ['compile']); 214 | }); 215 | 216 | gulp.task('clean', ['clean:dist', 'clean:tmp', 'clean:build']); 217 | 218 | gulp.task('build', ['clean', 'compile']); 219 | gulp.task('build:watch', ['build', 'watch']); 220 | gulp.task('default', ['build:watch']); 221 | 222 | /** 223 | * Deletes the specified folder 224 | */ 225 | function deleteFolders(folders) { 226 | return del(folders); 227 | } 228 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-particle", 3 | "version": "1.0.4", 4 | "scripts": { 5 | "build": "gulp build", 6 | "build:watch": "gulp", 7 | "docs": "npm run docs:build", 8 | "docs:build": "compodoc -p tsconfig.json -n angular-particle -d docs --hideGenerator", 9 | "docs:serve": "npm run docs:build -- -s", 10 | "docs:watch": "npm run docs:build -- -s -w", 11 | "lint": "tslint --type-check --project tsconfig.json src/**/*.ts", 12 | "test": "tsc && karma start" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/ryuKKu-/angular-particle" 17 | }, 18 | "author": { 19 | "name": "Luc Raymond", 20 | "email": "ryukku.raymond@gmail.com" 21 | }, 22 | "keywords": [ 23 | "angular2", 24 | "angular4", 25 | "angular", 26 | "particlesjs", 27 | "particle", 28 | "typescript", 29 | "ng2", 30 | "particles" 31 | ], 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/ryuKKu-/angular-particle/issues" 35 | }, 36 | "devDependencies": { 37 | "@angular/common": "^7.1.0", 38 | "@angular/compiler": "^7.1.0", 39 | "@angular/compiler-cli": "^7.1.0", 40 | "@angular/core": "^7.1.0", 41 | "@angular/platform-browser": "^7.1.0", 42 | "@angular/platform-browser-dynamic": "^7.1.0", 43 | "@compodoc/compodoc": "^1.1.6", 44 | "@types/jasmine": "3.3.0", 45 | "@types/node": "~10.12.10", 46 | "codelyzer": "~4.5.0", 47 | "core-js": "^2.5.7", 48 | "del": "^3.0.0", 49 | "gulp": "^3.9.1", 50 | "gulp-rename": "^1.4.0", 51 | "gulp-rollup": "^2.16.2", 52 | "jasmine-core": "~3.3.0", 53 | "jasmine-spec-reporter": "~4.2.1", 54 | "karma": "~3.1.1", 55 | "karma-chrome-launcher": "~2.2.0", 56 | "karma-cli": "~1.0.1", 57 | "karma-coverage-istanbul-reporter": "^2.0.4", 58 | "karma-jasmine": "~2.0.1", 59 | "karma-jasmine-html-reporter": "^1.4.0", 60 | "node-sass": "^4.10.0", 61 | "node-sass-tilde-importer": "^1.0.2", 62 | "node-watch": "^0.5.9", 63 | "protractor": "~5.4.1", 64 | "rollup": "^0.67.3", 65 | "run-sequence": "^2.2.1", 66 | "rxjs": "^6.3.3", 67 | "ts-node": "~7.0.1", 68 | "tslint": "~5.11.0", 69 | "typescript": "~3.1.6", 70 | "zone.js": "^0.8.26" 71 | }, 72 | "engines": { 73 | "node": ">=6.0.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ParticlesComponent } from './particles.component'; 5 | import { ParticlesDirective } from './particles.directive'; 6 | 7 | export * from './particles.component'; 8 | export * from './particles.directive'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule 13 | ], 14 | declarations: [ 15 | ParticlesComponent, 16 | ParticlesDirective 17 | ], 18 | exports: [ 19 | ParticlesComponent, 20 | ParticlesDirective 21 | ] 22 | }) 23 | export class ParticlesModule { } -------------------------------------------------------------------------------- /src/lib/CanvasManager.ts: -------------------------------------------------------------------------------- 1 | import { ParticlesManager, ICanvasParams, IParams, ITmpParams, hexToRgb } from './index'; 2 | 3 | export class CanvasManager { 4 | public particlesManager: ParticlesManager; 5 | 6 | constructor(private _canvasParams: ICanvasParams, private _params: IParams, private _tmpParams: ITmpParams) { 7 | this._onWindowResize = this._onWindowResize.bind(this); 8 | 9 | this._retinaInit(); 10 | this._canvasSize(); 11 | 12 | this.particlesManager = new ParticlesManager(this._canvasParams, this._params, this._tmpParams); 13 | this.particlesManager.particlesCreate(); 14 | 15 | this._densityAutoParticles(); 16 | 17 | let { particles } = this._params; 18 | particles.line_linked.color_rgb_line = hexToRgb(particles.line_linked.color); 19 | } 20 | 21 | public cancelAnimation(): void { 22 | if (!this._tmpParams.drawAnimFrame) { 23 | return; 24 | } 25 | cancelAnimationFrame(this._tmpParams.drawAnimFrame); 26 | this._tmpParams.drawAnimFrame = null; 27 | } 28 | 29 | public draw(): void { 30 | let { particles } = this._params; 31 | 32 | if (particles.shape.type == 'image') { 33 | if (this._tmpParams.img_type == 'svg') { 34 | if (this._tmpParams.count_svg >= particles.number.value) { 35 | this.particlesManager.particlesDraw(); 36 | if (!particles.move.enable) { 37 | cancelAnimationFrame(this._tmpParams.drawAnimFrame); 38 | } else { 39 | this._tmpParams.drawAnimFrame = requestAnimationFrame(this.draw.bind(this)); 40 | } 41 | } else { 42 | if (!this._tmpParams.img_error) { 43 | this._tmpParams.drawAnimFrame = requestAnimationFrame(this.draw.bind(this)); 44 | } 45 | } 46 | } else { 47 | if (this._tmpParams.img_obj != undefined) { 48 | this.particlesManager.particlesDraw(); 49 | if (!particles.move.enable) { 50 | cancelAnimationFrame(this._tmpParams.drawAnimFrame); 51 | } else { 52 | this._tmpParams.drawAnimFrame = requestAnimationFrame(this.draw.bind(this)); 53 | } 54 | } else { 55 | if (!this._tmpParams.img_error) { 56 | this._tmpParams.drawAnimFrame = requestAnimationFrame(this.draw.bind(this)); 57 | } 58 | } 59 | } 60 | } else { 61 | this.particlesManager.particlesDraw(); 62 | 63 | if (!particles.move.enable) { 64 | cancelAnimationFrame(this._tmpParams.drawAnimFrame); 65 | } else { 66 | this._tmpParams.drawAnimFrame = requestAnimationFrame(this.draw.bind(this)); 67 | } 68 | } 69 | } 70 | 71 | private _densityAutoParticles(): void { 72 | let { particles } = this._params; 73 | 74 | if (particles.number.density.enable) { 75 | let area: number = this._canvasParams.el.width * this._canvasParams.el.height / 1000; 76 | 77 | if (this._tmpParams.retina) { 78 | area = area / (this._canvasParams.pxratio * 2); 79 | } 80 | 81 | let nb_particles: number = area * particles.number.value / particles.number.density.value_area; 82 | 83 | let missing_particles: number = particles.array.length - nb_particles; 84 | 85 | if (missing_particles < 0) { 86 | this.particlesManager.pushParticles(Math.abs(missing_particles)); 87 | } else { 88 | this.particlesManager.removeParticles(missing_particles); 89 | } 90 | } 91 | } 92 | 93 | private _retinaInit(): void { 94 | if (this._params.retina_detect && window.devicePixelRatio > 1) { 95 | this._canvasParams.pxratio = window.devicePixelRatio; 96 | this._tmpParams.retina = true; 97 | 98 | this._canvasParams.width = this._canvasParams.el.offsetWidth * this._canvasParams.pxratio; 99 | this._canvasParams.height = this._canvasParams.el.offsetHeight * this._canvasParams.pxratio; 100 | 101 | this._params.particles.size.value = this._tmpParams.obj.size_value * this._canvasParams.pxratio; 102 | this._params.particles.size.anim.speed = this._tmpParams.obj.size_anim_speed * this._canvasParams.pxratio; 103 | this._params.particles.move.speed = this._tmpParams.obj.move_speed * this._canvasParams.pxratio; 104 | this._params.particles.line_linked.distance = this._tmpParams.obj.line_linked_distance * this._canvasParams.pxratio; 105 | this._params.interactivity.modes.grab.distance = this._tmpParams.obj.mode_grab_distance * this._canvasParams.pxratio; 106 | this._params.interactivity.modes.bubble.distance = this._tmpParams.obj.mode_bubble_distance * this._canvasParams.pxratio; 107 | this._params.particles.line_linked.width = this._tmpParams.obj.line_linked_width * this._canvasParams.pxratio; 108 | this._params.interactivity.modes.bubble.size = this._tmpParams.obj.mode_bubble_size * this._canvasParams.pxratio; 109 | this._params.interactivity.modes.repulse.distance = this._tmpParams.obj.mode_repulse_distance * this._canvasParams.pxratio; 110 | 111 | } else { 112 | this._canvasParams.pxratio = 1; 113 | this._tmpParams.retina = false; 114 | } 115 | } 116 | 117 | private _canvasClear(): void { 118 | this._canvasParams.ctx.clearRect(0, 0, this._canvasParams.width, this._canvasParams.height); 119 | } 120 | 121 | private _canvasPaint(): void { 122 | this._canvasParams.ctx.fillRect(0, 0, this._canvasParams.width, this._canvasParams.height); 123 | } 124 | 125 | private _canvasSize(): void { 126 | this._canvasParams.el.width = this._canvasParams.width; 127 | this._canvasParams.el.height = this._canvasParams.height; 128 | 129 | if (this._params && this._params.interactivity.events.resize) { 130 | window.addEventListener('resize', this._onWindowResize); 131 | } 132 | } 133 | 134 | private _onWindowResize(): void { 135 | this._canvasParams.width = this._canvasParams.el.offsetWidth; 136 | this._canvasParams.height = this._canvasParams.el.offsetHeight; 137 | 138 | if (this._tmpParams.retina) { 139 | this._canvasParams.width *= this._canvasParams.pxratio; 140 | this._canvasParams.height *= this._canvasParams.pxratio; 141 | } 142 | 143 | this._canvasParams.el.width = this._canvasParams.width; 144 | this._canvasParams.el.height = this._canvasParams.height; 145 | 146 | if (!this._params.particles.move.enable) { 147 | this.particlesManager.particlesEmpty(); 148 | this.particlesManager.particlesCreate(); 149 | this.particlesManager.particlesDraw(); 150 | this._densityAutoParticles(); 151 | } 152 | 153 | this._densityAutoParticles(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/lib/Particle.ts: -------------------------------------------------------------------------------- 1 | import { IParams, ICanvasParams, ITmpParams, hexToRgb, getColor, createSvgImg }  from './index'; 2 | 3 | export class Particle { 4 | radius: number; 5 | radius_bubble: number; 6 | size_status: boolean; 7 | vs: number; 8 | 9 | x: number; 10 | y: number; 11 | color: any; 12 | 13 | opacity: number; 14 | opacity_bubble: number; 15 | opacity_status: boolean; 16 | vo: number; 17 | 18 | vx: number; 19 | vy: number; 20 | 21 | vx_i: number; 22 | vy_i: number; 23 | 24 | shape: string; 25 | 26 | img: { src: string; ratio: number; loaded?: boolean; obj?: any; }; 27 | 28 | constructor(private _canvasParams: ICanvasParams, private _params: IParams, private _tmpParams: ITmpParams, color?: any, opacity?: any, position?: { x: number; y: number; }) { 29 | this._setupSize(); 30 | this._setupPosition(position); 31 | this._setupColor(color); 32 | this._setupOpacity(); 33 | this._setupAnimation(); 34 | } 35 | 36 | private _setupSize(): void { 37 | this.radius = (this._params.particles.size.random ? Math.random() : 1) * this._params.particles.size.value; 38 | if (this._params.particles.size.anim.enable) { 39 | this.size_status = false; 40 | this.vs = this._params.particles.size.anim.speed / 100; 41 | if (!this._params.particles.size.anim.sync) 42 | this.vs = this.vs * Math.random(); 43 | } 44 | } 45 | 46 | private _setupPosition(position?: { x: number; y: number; }): void { 47 | this.x = position ? position.x : Math.random() * this._canvasParams.width; 48 | this.y = position ? position.y : Math.random() * this._canvasParams.height; 49 | 50 | if (this.x > this._canvasParams.width - this.radius * 2) { 51 | this.x = this.x - this.radius; 52 | } else if (this.x < this.radius * 2) { 53 | this.x = this.x + this.radius; 54 | } 55 | if (this.y > this._canvasParams.height - this.radius * 2) { 56 | this.y = this.y - this.radius; 57 | } else if (this.y < this.radius * 2) { 58 | this.y = this.y + this.radius; 59 | } 60 | 61 | if (this._params.particles.move.bounce) { 62 | this._checkOverlap(this, position); 63 | } 64 | } 65 | 66 | private _checkOverlap(p1: Particle, position?: { x: number; y: number; }): void { 67 | let { particles } = this._params; 68 | 69 | particles.array.forEach((particle: Particle) => { 70 | let p2: Particle = particle; 71 | 72 | let dx: number = p1.x - p2.x; 73 | let dy: number = p1.y - p2.y; 74 | let dist: number = Math.sqrt(dx * dx + dy * dy); 75 | 76 | if (dist <= p1.radius + p2.radius) { 77 | p1.x = position ? position.x : Math.random() * this._canvasParams.width; 78 | p1.y = position ? position.y : Math.random() * this._canvasParams.height; 79 | this._checkOverlap(p1); 80 | } 81 | }); 82 | } 83 | 84 | private _setupColor(color?: any) { 85 | this.color = getColor(color.value); 86 | } 87 | 88 | private _setupOpacity(): void { 89 | this.opacity = (this._params.particles.opacity.random ? Math.random() : 1) * this._params.particles.opacity.value; 90 | if (this._params.particles.opacity.anim.enable) { 91 | this.opacity_status = false; 92 | this.vo = this._params.particles.opacity.anim.speed / 100; 93 | if (!this._params.particles.opacity.anim.sync) { 94 | this.vo = this.vo * Math.random(); 95 | } 96 | } 97 | } 98 | 99 | private _setupAnimation(): void { 100 | let velbase: { x: number; y: number; } = null; 101 | switch (this._params.particles.move.direction) { 102 | case 'top': 103 | velbase = { x: 0, y: -1 }; 104 | break; 105 | case 'top-right': 106 | velbase = { x: 0.5, y: -0.5 }; 107 | break; 108 | case 'right': 109 | velbase = { x: 1, y: 0 }; 110 | break; 111 | case 'bottom-right': 112 | velbase = {  x: 0.5, y: 0.5 }; 113 | break; 114 | case 'bottom': 115 | velbase = { x: 0, y: 1 }; 116 | break; 117 | case 'bottom-left': 118 | velbase = { x: -0.5, y: 1 }; 119 | break; 120 | case 'left': 121 | velbase = { x: -1, y: 0 }; 122 | break; 123 | case 'top-left': 124 | velbase = {  x: -0.5, y: -0.5 }; 125 | break; 126 | default: 127 | velbase = {  x: 0, y: 0 }; 128 | break; 129 | } 130 | 131 | if (this._params.particles.move.straight) { 132 | this.vx = velbase.x; 133 | this.vy = velbase.y; 134 | if (this._params.particles.move.random) { 135 | this.vx = this.vx * (Math.random()); 136 | this.vy = this.vy * (Math.random()); 137 | } 138 | } else { 139 | this.vx = velbase.x + Math.random() - 0.5; 140 | this.vy = velbase.y + Math.random() - 0.5; 141 | } 142 | 143 | this.vx_i = this.vx; 144 | this.vy_i = this.vy; 145 | 146 | let shape_type: any = this._params.particles.shape.type; 147 | 148 | if (typeof (shape_type) == 'object') { 149 | if (shape_type instanceof Array) { 150 | let shape_selected: string = shape_type[Math.floor(Math.random() * shape_type.length)]; 151 | this.shape = shape_selected; 152 | } 153 | } else { 154 | this.shape = shape_type; 155 | } 156 | 157 | if (this.shape == 'image') { 158 | let sh: any = this._params.particles.shape; 159 | this.img = { 160 | src: sh.image.src, 161 | ratio: sh.image.width / sh.image.height 162 | }; 163 | 164 | if (!this.img.ratio) 165 | this.img.ratio = 1; 166 | if (this._tmpParams.img_type == 'svg' && this._tmpParams.source_svg != undefined) { 167 | createSvgImg(this, this._tmpParams); 168 | if (this._tmpParams.pushing) { 169 | this.img.loaded = false; 170 | } 171 | } 172 | } 173 | } 174 | 175 | private _drawShape(c: CanvasRenderingContext2D, startX: number, startY: number, sideLength: number, sideCountNumerator: number, sideCountDenominator: number): void { 176 | let sideCount: number = sideCountNumerator * sideCountDenominator; 177 | let decimalSides: number = sideCountNumerator / sideCountDenominator; 178 | let interiorAngleDegrees: number = (180 * (decimalSides - 2)) / decimalSides; 179 | let interiorAngle: number = Math.PI - Math.PI * interiorAngleDegrees / 180; 180 | 181 | c.save(); 182 | c.beginPath(); 183 | c.translate(startX, startY); 184 | c.moveTo(0, 0); 185 | 186 | for (let i = 0; i < sideCount; i++) { 187 | c.lineTo(sideLength, 0); 188 | c.translate(sideLength, 0); 189 | c.rotate(interiorAngle); 190 | } 191 | 192 | c.fill(); 193 | c.restore(); 194 | } 195 | 196 | public draw(): void { 197 | let { particles } = this._params; 198 | 199 | let radius: number; 200 | if (this.radius_bubble != undefined) { 201 | radius = this.radius_bubble; 202 | } else { 203 | radius = this.radius; 204 | } 205 | 206 | let opacity: number; 207 | if (this.opacity_bubble != undefined) { 208 | opacity = this.opacity_bubble; 209 | } else { 210 | opacity = this.opacity; 211 | } 212 | 213 | let color_value: string; 214 | 215 | if (this.color.rgb) { 216 | let { r, g, b } = this.color.rgb; 217 | color_value = `rgba( ${r}, ${g}, ${b}, ${opacity} )`; 218 | } else { 219 | let { h, s, l } = this.color.hsl; 220 | color_value = `hsla( ${h}, ${s}, ${l}, ${opacity} )`; 221 | } 222 | 223 | this._canvasParams.ctx.fillStyle = color_value; 224 | this._canvasParams.ctx.beginPath(); 225 | 226 | switch (this.shape) { 227 | case 'circle': 228 | this._canvasParams.ctx.arc(this.x, this.y, radius, 0, Math.PI * 2, false); 229 | break; 230 | 231 | case 'edge': 232 | this._canvasParams.ctx.rect(this.x - radius, this.y - radius, radius * 2, radius * 2); 233 | break; 234 | 235 | case 'triangle': 236 | this._drawShape(this._canvasParams.ctx, this.x - radius, this.y + radius / 1.66, radius * 2, 3, 2); 237 | break; 238 | 239 | case 'polygon': 240 | this._drawShape( 241 | this._canvasParams.ctx, 242 | this.x - radius / (this._params.particles.shape.polygon.nb_sides / 3.5), 243 | this.y - radius / (2.66 / 3.5), 244 | radius * 2.66 / (this._params.particles.shape.polygon.nb_sides / 3), 245 | this._params.particles.shape.polygon.nb_sides, 246 | 1 247 | ); 248 | break; 249 | 250 | case 'star': 251 | this._drawShape( 252 | this._canvasParams.ctx, 253 | this.x - radius * 2 / (this._params.particles.shape.polygon.nb_sides / 4), 254 | this.y - radius / (2 * 2.66 / 3.5), 255 | radius * 2 * 2.66 / (this._params.particles.shape.polygon.nb_sides / 3), 256 | this._params.particles.shape.polygon.nb_sides, 257 | 2 258 | ); 259 | break; 260 | 261 | case 'image': 262 | let draw: (img_obj: any) => void = 263 | (img_obj) => { 264 | this._canvasParams.ctx.drawImage( 265 | img_obj, 266 | this.x - radius, 267 | this.y - radius, 268 | radius * 2, 269 | radius * 2 / this.img.ratio 270 | ); 271 | }; 272 | let img_obj: any; 273 | 274 | if (this._tmpParams.img_type == 'svg') { 275 | img_obj = this.img.obj; 276 | } else { 277 | img_obj = this._tmpParams.img_obj; 278 | } 279 | 280 | if (img_obj) 281 | draw(img_obj); 282 | break; 283 | } 284 | 285 | this._canvasParams.ctx.closePath(); 286 | 287 | if (this._params.particles.shape.stroke.width > 0) { 288 | this._canvasParams.ctx.strokeStyle = this._params.particles.shape.stroke.color; 289 | this._canvasParams.ctx.lineWidth = this._params.particles.shape.stroke.width; 290 | this._canvasParams.ctx.stroke(); 291 | } 292 | 293 | this._canvasParams.ctx.fill(); 294 | } 295 | } -------------------------------------------------------------------------------- /src/lib/ParticleInteraction.ts: -------------------------------------------------------------------------------- 1 | import { Particle, IParams, ICanvasParams } from './index'; 2 | 3 | export class ParticleInteraction { 4 | constructor() { } 5 | 6 | linkParticles(p1: Particle, p2: Particle, params: IParams, canvasParams: ICanvasParams): void { 7 | let dx: number = p1.x - p2.x; 8 | let dy: number = p1.y - p2.y; 9 | let dist: number = Math.sqrt(dx * dx + dy * dy); 10 | let { line_linked } = params.particles; 11 | 12 | if (dist <= params.particles.line_linked.distance) { 13 | let opacity_line: number = params.particles.line_linked.opacity - (dist / (1 / params.particles.line_linked.opacity)) / params.particles.line_linked.distance; 14 | if (opacity_line > 0) { 15 | let color_line: any = params.particles.line_linked.color_rgb_line; 16 | let { r, g, b } = color_line; 17 | canvasParams.ctx.save(); 18 | canvasParams.ctx.strokeStyle = `rgba( ${r}, ${g}, ${b}, ${opacity_line} )`; 19 | canvasParams.ctx.lineWidth = params.particles.line_linked.width; 20 | 21 | canvasParams.ctx.beginPath(); 22 | if (line_linked.shadow.enable) { 23 | canvasParams.ctx.shadowBlur = line_linked.shadow.blur; 24 | canvasParams.ctx.shadowColor = line_linked.shadow.color; 25 | } 26 | 27 | canvasParams.ctx.moveTo(p1.x, p1.y); 28 | canvasParams.ctx.lineTo(p2.x, p2.y); 29 | canvasParams.ctx.stroke(); 30 | canvasParams.ctx.closePath(); 31 | canvasParams.ctx.restore(); 32 | } 33 | } 34 | } 35 | 36 | attractParticles(p1: Particle, p2: Particle, params: IParams): void { 37 | let dx: number = p1.x - p2.x; 38 | let dy: number = p1.y - p2.y; 39 | let dist: number = Math.sqrt(dx * dx + dy * dy); 40 | 41 | if (dist <= params.particles.line_linked.distance) { 42 | let ax = dx / (params.particles.move.attract.rotateX * 1000); 43 | let ay = dy / (params.particles.move.attract.rotateY * 1000); 44 | 45 | p1.vx -= ax; 46 | p1.vy -= ay; 47 | 48 | p2.vx += ax; 49 | p2.vy += ay; 50 | } 51 | } 52 | 53 | bounceParticles(p1: Particle, p2: Particle): void { 54 | let dx: number = p1.x - p2.x; 55 | let dy: number = p1.y - p2.y; 56 | let dist: number = Math.sqrt(dx * dx + dy * dy); 57 | let dist_p: number = p1.radius + p2.radius; 58 | 59 | if (dist <= dist_p) { 60 | p1.vx = -p1.vx; 61 | p1.vy = -p1.vy; 62 | p2.vx = -p2.vx; 63 | p2.vy = -p2.vy; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/lib/ParticlesManager.ts: -------------------------------------------------------------------------------- 1 | import { Particle, ParticleInteraction, IParams, ICanvasParams, IMouseParams, ITmpParams, isInArray, clamp } from './index'; 2 | 3 | export class ParticlesManager { 4 | private _interaction: ParticleInteraction; 5 | 6 | constructor(private _canvasParams: ICanvasParams, private _params: IParams, private _tmpParams: ITmpParams) { 7 | this._interaction = new ParticleInteraction(); 8 | } 9 | 10 | public particlesCreate(): void { 11 | let { color, opacity } = this._params.particles; 12 | for (let i = 0; i < this._params.particles.number.value; i++) { 13 | this._params.particles.array.push(new Particle(this._canvasParams, this._params, this._tmpParams, color, opacity.value)); 14 | } 15 | } 16 | 17 | private _particlesUpdate(): void { 18 | type Pos = { 19 | x_left: number; 20 | x_right: number; 21 | y_top: number; 22 | y_bottom: number; 23 | }; 24 | 25 | this._params.particles.array.forEach((particle: Particle, i: number) => { 26 | if (this._params.particles.move.enable) { 27 | let ms = this._params.particles.move.speed / 2; 28 | particle.x += particle.vx * ms; 29 | particle.y += particle.vy * ms; 30 | } 31 | 32 | if (this._params.particles.opacity.anim.enable) { 33 | if (particle.opacity_status == true) { 34 | if (particle.opacity >= this._params.particles.opacity.value) 35 | particle.opacity_status = false; 36 | particle.opacity += particle.vo; 37 | } else { 38 | if (particle.opacity <= this._params.particles.opacity.anim.opacity_min) 39 | particle.opacity_status = true; 40 | particle.opacity -= particle.vo; 41 | } 42 | if (particle.opacity < 0) 43 | particle.opacity = 0; 44 | } 45 | 46 | if (this._params.particles.size.anim.enable) { 47 | if (particle.size_status == true) { 48 | if (particle.radius >= this._params.particles.size.value) 49 | particle.size_status = false; 50 | particle.radius += particle.vs; 51 | } else { 52 | if (particle.radius <= this._params.particles.size.anim.size_min) 53 | particle.size_status = true; 54 | particle.radius -= particle.vs; 55 | } 56 | if (particle.radius < 0) 57 | particle.radius = 0; 58 | } 59 | 60 | let new_pos: Pos; 61 | 62 | if (this._params.particles.move.out_mode == 'bounce') { 63 | new_pos = { 64 | x_left: particle.radius, 65 | x_right: this._canvasParams.width, 66 | y_top: particle.radius, 67 | y_bottom: this._canvasParams.height 68 | }; 69 | } else { 70 | new_pos = { 71 | x_left: -particle.radius, 72 | x_right: this._canvasParams.width + particle.radius, 73 | y_top: -particle.radius, 74 | y_bottom: this._canvasParams.height + particle.radius 75 | }; 76 | } 77 | 78 | if (particle.x - particle.radius > this._canvasParams.width) { 79 | particle.x = new_pos.x_left; 80 | particle.y = Math.random() * this._canvasParams.height; 81 | } else if (particle.x + particle.radius < 0) { 82 | particle.x = new_pos.x_right; 83 | particle.y = Math.random() * this._canvasParams.height; 84 | } 85 | 86 | if (particle.y - particle.radius > this._canvasParams.height) { 87 | particle.y = new_pos.y_top; 88 | particle.x = Math.random() * this._canvasParams.width; 89 | } else if (particle.y + particle.radius < 0) { 90 | particle.y = new_pos.y_bottom; 91 | particle.x = Math.random() * this._canvasParams.width; 92 | } 93 | 94 | switch (this._params.particles.move.out_mode) { 95 | case 'bounce': 96 | if (particle.x + particle.radius > this._canvasParams.width) 97 | particle.vx = -particle.vx; 98 | else if (particle.x - particle.radius < 0) 99 | particle.vx = -particle.vx; 100 | if (particle.y + particle.radius > this._canvasParams.height) 101 | particle.vy = -particle.vy; 102 | else if (particle.y - particle.radius < 0) 103 | particle.vy = -particle.vy; 104 | break; 105 | } 106 | 107 | if (isInArray('grab', this._params.interactivity.events.onhover.mode)) { 108 | this._grabParticle(particle); 109 | } 110 | 111 | if (isInArray('bubble', this._params.interactivity.events.onhover.mode) || 112 | isInArray('bubble', this._params.interactivity.events.onclick.mode)) { 113 | this._bubbleParticle(particle); 114 | } 115 | 116 | if (isInArray('repulse', this._params.interactivity.events.onhover.mode) || 117 | isInArray('repulse', this._params.interactivity.events.onclick.mode)) { 118 | this._repulseParticle(particle); 119 | } 120 | 121 | if (this._params.particles.line_linked.enable || this._params.particles.move.attract.enable) { 122 | for (let j = i + 1; j < this._params.particles.array.length; j++) { 123 | let link = this._params.particles.array[j]; 124 | 125 | if (this._params.particles.line_linked.enable) 126 | this._interaction.linkParticles(particle, link, this._params, this._canvasParams); 127 | 128 | if (this._params.particles.move.attract.enable) 129 | this._interaction.attractParticles(particle, link, this._params); 130 | 131 | if (this._params.particles.move.bounce) 132 | this._interaction.bounceParticles(particle, link); 133 | } 134 | } 135 | }); 136 | } 137 | 138 | public particlesDraw(): void { 139 | this._canvasParams.ctx.clearRect(0, 0, this._canvasParams.width, this._canvasParams.height); 140 | this._particlesUpdate(); 141 | 142 | this._params.particles.array.forEach((particle: Particle) => { 143 | particle.draw(); 144 | }); 145 | } 146 | 147 | public particlesEmpty(): void { 148 | this._params.particles.array = []; 149 | } 150 | 151 | public removeParticles(nb: number): void { 152 | this._params.particles.array.splice(0, nb); 153 | 154 | if (!this._params.particles.move.enable) { 155 | this.particlesDraw(); 156 | } 157 | } 158 | 159 | public pushParticles(nb: number, pos?: IMouseParams): void { 160 | this._tmpParams.pushing = true; 161 | 162 | for (let i = 0; i < nb; i++) { 163 | this._params.particles.array.push( 164 | new Particle( 165 | this._canvasParams, 166 | this._params, 167 | this._tmpParams, 168 | this._params.particles.color, 169 | this._params.particles.opacity.value, 170 | { 171 | x: pos ? pos.pos_x : Math.random() * this._canvasParams.width, 172 | y: pos ? pos.pos_y : Math.random() * this._canvasParams.height 173 | }) 174 | ); 175 | 176 | if (i == nb - 1) { 177 | if (!this._params.particles.move.enable) { 178 | this.particlesDraw(); 179 | } 180 | this._tmpParams.pushing = false; 181 | } 182 | } 183 | } 184 | 185 | private _bubbleParticle(particle: Particle) { 186 | if (this._params.interactivity.events.onhover.enable && 187 | isInArray('bubble', this._params.interactivity.events.onhover.mode)) { 188 | 189 | let dx_mouse: number = particle.x - this._params.interactivity.mouse.pos_x; 190 | let dy_mouse: number = particle.y - this._params.interactivity.mouse.pos_y; 191 | let dist_mouse: number = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse); 192 | let ratio: number = 1 - dist_mouse / this._params.interactivity.modes.bubble.distance; 193 | 194 | let init: () => void = 195 | () => { 196 | particle.opacity_bubble = particle.opacity; 197 | particle.radius_bubble = particle.radius; 198 | }; 199 | 200 | if (dist_mouse <= this._params.interactivity.modes.bubble.distance) { 201 | if (ratio >= 0 && this._params.interactivity.status == 'mousemove') { 202 | 203 | if (this._params.interactivity.modes.bubble.size != this._params.particles.size.value) { 204 | if (this._params.interactivity.modes.bubble.size > this._params.particles.size.value) { 205 | let size: number = particle.radius + (this._params.interactivity.modes.bubble.size * ratio); 206 | if (size >= 0) { 207 | particle.radius_bubble = size; 208 | } 209 | } else { 210 | let dif: number = particle.radius - this._params.interactivity.modes.bubble.size; 211 | let size: number = particle.radius - (dif * ratio); 212 | if (size > 0) { 213 | particle.radius_bubble = size; 214 | } else { 215 | particle.radius_bubble = 0; 216 | } 217 | } 218 | } 219 | 220 | if (this._params.interactivity.modes.bubble.opacity != this._params.particles.opacity.value) { 221 | if (this._params.interactivity.modes.bubble.opacity > this._params.particles.opacity.value) { 222 | let opacity: number = this._params.interactivity.modes.bubble.opacity * ratio; 223 | if (opacity > particle.opacity && opacity <= this._params.interactivity.modes.bubble.opacity) { 224 | particle.opacity_bubble = opacity; 225 | } 226 | } else { 227 | let opacity: number = particle.opacity - (this._params.particles.opacity.value - this._params.interactivity.modes.bubble.opacity) * ratio; 228 | if (opacity < particle.opacity && opacity >= this._params.interactivity.modes.bubble.opacity) { 229 | particle.opacity_bubble = opacity; 230 | } 231 | } 232 | } 233 | } 234 | } else { 235 | init(); 236 | } 237 | 238 | if (this._params.interactivity.status == 'mouseleave') { 239 | init(); 240 | } 241 | 242 | } else if (this._params.interactivity.events.onclick.enable && 243 | isInArray('bubble', this._params.interactivity.events.onclick.mode)) { 244 | 245 | if (this._tmpParams.bubble_clicking) { 246 | let dx_mouse: number = particle.x - this._params.interactivity.mouse.click_pos_x; 247 | let dy_mouse: number = particle.y - this._params.interactivity.mouse.click_pos_y; 248 | let dist_mouse: number = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse); 249 | let time_spent: number = (new Date().getTime() - this._params.interactivity.mouse.click_time) / 1000; 250 | 251 | if (time_spent > this._params.interactivity.modes.bubble.duration) { 252 | this._tmpParams.bubble_duration_end = true; 253 | } 254 | 255 | if (time_spent > this._params.interactivity.modes.bubble.duration * 2) { 256 | this._tmpParams.bubble_clicking = false; 257 | this._tmpParams.bubble_duration_end = false; 258 | } 259 | 260 | let process: any = (bubble_param: any, particles_param: any, p_obj_bubble: any, p_obj: any, id: any) => { 261 | if (bubble_param != particles_param) { 262 | if (!this._tmpParams.bubble_duration_end) { 263 | if (dist_mouse <= this._params.interactivity.modes.bubble.distance) { 264 | let obj: any; 265 | if (p_obj_bubble != undefined) { 266 | obj = p_obj_bubble; 267 | } else { 268 | obj = p_obj; 269 | } 270 | if (obj != bubble_param) { 271 | let value: any = p_obj - (time_spent * (p_obj - bubble_param) / this._params.interactivity.modes.bubble.duration); 272 | if (id == 'size') 273 | particle.radius_bubble = value; 274 | if (id == 'opacity') 275 | particle.opacity_bubble = value; 276 | } 277 | } else { 278 | if (id == 'size') 279 | particle.radius_bubble = undefined; 280 | if (id == 'opacity') 281 | particle.opacity_bubble = undefined; 282 | } 283 | } else { 284 | if (p_obj_bubble != undefined) { 285 | let value_tmp: any = p_obj - (time_spent * (p_obj - bubble_param) / this._params.interactivity.modes.bubble.duration); 286 | let dif: any = bubble_param - value_tmp; 287 | let value: any = bubble_param + dif; 288 | if (id == 'size') 289 | particle.radius_bubble = value; 290 | if (id == 'opacity') 291 | particle.opacity_bubble = value; 292 | } 293 | } 294 | } 295 | }; 296 | 297 | if (this._tmpParams.bubble_clicking) { 298 | process(this._params.interactivity.modes.bubble.size, this._params.particles.size.value, particle.radius_bubble, particle.radius, 'size'); 299 | process(this._params.interactivity.modes.bubble.opacity, this._params.particles.opacity.value, particle.opacity_bubble, particle.opacity, 'opacity'); 300 | } 301 | } 302 | } 303 | } 304 | 305 | private _repulseParticle(particle: Particle) { 306 | if (this._params.interactivity.events.onhover.enable && 307 | isInArray('repulse', this._params.interactivity.events.onhover.mode) && 308 | this._params.interactivity.status == 'mousemove') { 309 | 310 | let dx_mouse: number = particle.x - this._params.interactivity.mouse.pos_x; 311 | let dy_mouse: number = particle.y - this._params.interactivity.mouse.pos_y; 312 | let dist_mouse: number = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse); 313 | 314 | let normVec: any = { x: dx_mouse / dist_mouse, y: dy_mouse / dist_mouse }; 315 | let repulseRadius: number = this._params.interactivity.modes.repulse.distance; 316 | let velocity: number = 100; 317 | let repulseFactor: number = clamp((1 / repulseRadius) * (-1 * Math.pow(dist_mouse / repulseRadius, 2) + 1) * repulseRadius * velocity, 0, 50); 318 | 319 | let pos = { 320 | x: particle.x + normVec.x * repulseFactor, 321 | y: particle.y + normVec.y * repulseFactor 322 | } 323 | 324 | if (this._params.particles.move.out_mode == 'bounce') { 325 | if (pos.x - particle.radius > 0 && pos.x + particle.radius < this._canvasParams.width) 326 | particle.x = pos.x; 327 | if (pos.y - particle.radius > 0 && pos.y + particle.radius < this._canvasParams.height) 328 | particle.y = pos.y; 329 | } else { 330 | particle.x = pos.x; 331 | particle.y = pos.y; 332 | } 333 | 334 | } else if (this._params.interactivity.events.onclick.enable && 335 | isInArray('repulse', this._params.interactivity.events.onclick.mode)) { 336 | 337 | if (!this._tmpParams.repulse_finish) { 338 | this._tmpParams.repulse_count++; 339 | if (this._tmpParams.repulse_count == this._params.particles.array.length) 340 | this._tmpParams.repulse_finish = true; 341 | } 342 | 343 | if (this._tmpParams.repulse_clicking) { 344 | 345 | let repulseRadius: number = Math.pow(this._params.interactivity.modes.repulse.distance / 6, 3); 346 | 347 | let dx: number = this._params.interactivity.mouse.click_pos_x - particle.x; 348 | let dy: number = this._params.interactivity.mouse.click_pos_y - particle.y; 349 | let d: number = dx * dx + dy * dy; 350 | 351 | let force: number = -repulseRadius / d * 1; 352 | 353 | let process: () => void = 354 | () => { 355 | let f: number = Math.atan2(dy, dx); 356 | particle.vx = force * Math.cos(f); 357 | particle.vy = force * Math.sin(f); 358 | if (this._params.particles.move.out_mode == 'bounce') { 359 | let pos: { 360 | x: number; 361 | y: number; 362 | } = { 363 | x: particle.x + particle.vx, 364 | y: particle.y + particle.vy 365 | } 366 | if (pos.x + particle.radius > this._canvasParams.width) 367 | particle.vx = -particle.vx; 368 | else if (pos.x - particle.radius < 0) 369 | particle.vx = -particle.vx; 370 | if (pos.y + particle.radius > this._canvasParams.height) 371 | particle.vy = -particle.vy; 372 | else if (pos.y - particle.radius < 0) 373 | particle.vy = -particle.vy; 374 | } 375 | }; 376 | 377 | if (d <= repulseRadius) { 378 | process(); 379 | } 380 | } else { 381 | if (this._tmpParams.repulse_clicking == false) { 382 | particle.vx = particle.vx_i; 383 | particle.vy = particle.vy_i; 384 | } 385 | } 386 | } 387 | } 388 | 389 | private _grabParticle(particle: Particle): void { 390 | let { interactivity, particles } = this._params; 391 | 392 | if (interactivity.events.onhover.enable && 393 | interactivity.status == 'mousemove') { 394 | 395 | let dx_mouse: number = particle.x - interactivity.mouse.pos_x; 396 | let dy_mouse: number = particle.y - interactivity.mouse.pos_y; 397 | let dist_mouse: number = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse); 398 | 399 | if (dist_mouse <= interactivity.modes.grab.distance) { 400 | 401 | let { grab } = interactivity.modes; 402 | 403 | let opacity_line: number = grab.line_linked.opacity - (dist_mouse / (1 / grab.line_linked.opacity)) / grab.distance; 404 | 405 | if (opacity_line > 0) { 406 | let color_line: { 407 | r: number; 408 | g: number; 409 | b: number; 410 | } = particles.line_linked.color_rgb_line; 411 | 412 | let { r, g, b } = color_line; 413 | this._canvasParams.ctx.strokeStyle = `rgba( ${r}, ${g}, ${b}, ${opacity_line} )`; 414 | this._canvasParams.ctx.lineWidth = particles.line_linked.width; 415 | 416 | this._canvasParams.ctx.beginPath(); 417 | this._canvasParams.ctx.moveTo(particle.x, particle.y); 418 | this._canvasParams.ctx.lineTo(interactivity.mouse.pos_x, interactivity.mouse.pos_y); 419 | this._canvasParams.ctx.stroke(); 420 | this._canvasParams.ctx.closePath(); 421 | } 422 | } 423 | } 424 | } 425 | } -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | export * from './utils'; 3 | export { CanvasManager } from './CanvasManager'; 4 | export { ParticlesManager } from './ParticlesManager'; 5 | export { Particle } from './Particle'; 6 | export { ParticleInteraction } from './ParticleInteraction'; -------------------------------------------------------------------------------- /src/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ICanvasParams { 2 | el: HTMLCanvasElement; 3 | width: number; 4 | height: number; 5 | pxratio?: number; 6 | ctx?: CanvasRenderingContext2D; 7 | } 8 | 9 | export interface ITmpParams { 10 | obj?: { 11 | size_value: number; 12 | size_anim_speed: number; 13 | move_speed: number; 14 | line_linked_distance: number; 15 | line_linked_width: number; 16 | mode_grab_distance: number; 17 | mode_bubble_distance: number; 18 | mode_bubble_size: number; 19 | mode_repulse_distance: number; 20 | }; 21 | retina?: boolean; 22 | img_type?: string; 23 | img_obj?: any; 24 | img_error?: any; 25 | source_svg?: any; 26 | count_svg?: number; 27 | pushing?: any; 28 | bubble_clicking?: boolean; 29 | bubble_duration_end?: boolean; 30 | repulse_clicking?: boolean; 31 | repulse_finish?: boolean; 32 | repulse_count?: number; 33 | checkAnimFrame?: any; 34 | drawAnimFrame?: any; 35 | }; 36 | 37 | export interface IMouseParams { 38 | pos_x?: number; 39 | pos_y?: number; 40 | click_pos_x?: number; 41 | click_pos_y?: number; 42 | click_time?: number; 43 | } 44 | 45 | export interface IParams { 46 | particles: { 47 | number: { 48 | value: number; 49 | density: { 50 | enable: boolean; 51 | value_area: number; 52 | } 53 | }; 54 | color: { 55 | value: any; 56 | }; 57 | shape: { 58 | type: string | string[]; 59 | stroke: { 60 | width: number; 61 | color: any; 62 | }, 63 | polygon: { 64 | nb_sides: number; 65 | }, 66 | image: { 67 | src: string; 68 | width: number; 69 | height: number; 70 | } 71 | }; 72 | opacity: { 73 | value: number; 74 | random: boolean; 75 | anim: { 76 | enable: boolean; 77 | speed: number; 78 | opacity_min: number; 79 | sync: boolean; 80 | } 81 | }; 82 | size: { 83 | value: number; 84 | random: boolean; 85 | anim: { 86 | enable: boolean; 87 | speed: number; 88 | size_min: number; 89 | sync: boolean; 90 | } 91 | }; 92 | line_linked: { 93 | enable: boolean; 94 | distance: number; 95 | color: any; 96 | opacity: number; 97 | width: number; 98 | color_rgb_line?: any; 99 | shadow: { 100 | enable: boolean; 101 | blur: number; 102 | color: string; 103 | }; 104 | }; 105 | move: { 106 | enable: boolean; 107 | speed: number; 108 | direction: string; 109 | random: boolean; 110 | straight: boolean; 111 | out_mode: string; 112 | bounce: boolean; 113 | attract: { 114 | enable: boolean; 115 | rotateX: number; 116 | rotateY: number; 117 | } 118 | }; 119 | array: any[]; 120 | }; 121 | interactivity: { 122 | el?: EventTarget; 123 | status?: string; 124 | detect_on: string; 125 | events: { 126 | onhover: { 127 | enable: boolean; 128 | mode: string | string[]; 129 | }; 130 | onclick: { 131 | enable: boolean; 132 | mode: string | string[]; 133 | }; 134 | resize: boolean; 135 | }; 136 | modes: { 137 | grab: { 138 | distance: number; 139 | line_linked: { 140 | opacity: number; 141 | } 142 | }; 143 | bubble: { 144 | distance: number; 145 | size: number; 146 | duration: number; 147 | opacity?: number; 148 | }; 149 | repulse: { 150 | distance: number; 151 | duration: number; 152 | }; 153 | push: { 154 | particles_nb: number; 155 | }; 156 | remove: { 157 | particles_nb: number; 158 | }; 159 | }; 160 | mouse?: IMouseParams; 161 | }; 162 | retina_detect: boolean; 163 | } -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { IParams, ITmpParams, Particle } from './index'; 2 | 3 | export type RGB = { 4 | r: number; 5 | g: number; 6 | b: number; 7 | }; 8 | 9 | export type HSL = { 10 | h: number; 11 | s: number; 12 | l: number; 13 | }; 14 | 15 | export const hexToRgb: (hex: string) => RGB = 16 | (hex) => { 17 | let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 18 | hex = hex.replace(shorthandRegex, (m, r, g, b) => { 19 | return r + r + g + g + b + b; 20 | }); 21 | let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 22 | return result ? { 23 | r: parseInt(result[1], 16), 24 | g: parseInt(result[2], 16), 25 | b: parseInt(result[3], 16) 26 | } : null; 27 | }; 28 | 29 | export const clamp: (number: number, min: number, max: number) => number = 30 | (number, min, max) => { 31 | return Math.min(Math.max(number, min), max); 32 | }; 33 | 34 | export const isInArray: (value: any, array: any) => boolean = 35 | (value, array) => { 36 | return array.indexOf(value) > -1; 37 | }; 38 | 39 | export const deepExtend: (destination: any, source: any) => any = 40 | function (destination, source) { 41 | for (let property in source) { 42 | if (source[property] && 43 | source[property].constructor && 44 | source[property].constructor === Object) { 45 | destination[property] = destination[property] || {}; 46 | deepExtend(destination[property], source[property]); 47 | } else { 48 | destination[property] = source[property]; 49 | } 50 | } 51 | return destination; 52 | }; 53 | 54 | export const getColor: (colorObject: any) => { rgb?: RGB, hsl?: HSL } = 55 | (colorObject) => { 56 | let color: { rgb?: RGB, hsl?: HSL } = {}; 57 | if (typeof (colorObject) == 'object') { 58 | if (colorObject instanceof Array) { 59 | let selectedColor: string = colorObject[Math.floor(Math.random() * colorObject.length)]; 60 | color.rgb = hexToRgb(selectedColor); 61 | } else { 62 | let { r, g, b } = colorObject; 63 | if (r !== undefined && g !== undefined && b !== undefined) { 64 | color.rgb = { r, g, b }; 65 | } else { 66 | let { h, s, l } = colorObject; 67 | if (h !== undefined && g !== undefined && b !== undefined) { 68 | color.hsl = { h, s, l }; 69 | } 70 | } 71 | } 72 | } else if (colorObject == 'random') { 73 | color.rgb = { 74 | r: (Math.floor(Math.random() * 255) + 1), 75 | g: (Math.floor(Math.random() * 255) + 1), 76 | b: (Math.floor(Math.random() * 255) + 1) 77 | } 78 | } else if (typeof (colorObject) == 'string') { 79 | color.rgb = hexToRgb(colorObject); 80 | } 81 | return color; 82 | }; 83 | 84 | export const getDefaultParams: () => IParams = 85 | () => { 86 | return { 87 | particles: { 88 | number: { 89 | value: 100, 90 | density: { 91 | enable: true, 92 | value_area: 800 93 | } 94 | }, 95 | color: { 96 | value: '#FFF' 97 | }, 98 | shape: { 99 | type: 'circle', 100 | stroke: { 101 | width: 0, 102 | color: '#000000' 103 | }, 104 | polygon: { 105 | nb_sides: 5 106 | }, 107 | image: { 108 | src: '', 109 | width: 100, 110 | height: 100 111 | } 112 | }, 113 | opacity: { 114 | value: 0.5, 115 | random: false, 116 | anim: { 117 | enable: true, 118 | speed: 1, 119 | opacity_min: 0.1, 120 | sync: false 121 | } 122 | }, 123 | size: { 124 | value: 3, 125 | random: true, 126 | anim: { 127 | enable: false, 128 | speed: 40, 129 | size_min: 0, 130 | sync: false 131 | } 132 | }, 133 | line_linked: { 134 | enable: true, 135 | distance: 150, 136 | color: '#FFF', 137 | opacity: 0.6, 138 | width: 1, 139 | shadow: { 140 | enable: false, 141 | blur: 5, 142 | color: 'lime' 143 | } 144 | }, 145 | move: { 146 | enable: true, 147 | speed: 3, 148 | direction: 'none', 149 | random: false, 150 | straight: false, 151 | out_mode: 'out', 152 | bounce: true, 153 | attract: { 154 | enable: false, 155 | rotateX: 3000, 156 | rotateY: 3000 157 | } 158 | }, 159 | array: [] 160 | }, 161 | interactivity: { 162 | detect_on: 'canvas', 163 | events: { 164 | onhover: { 165 | enable: true, 166 | mode: 'grab' 167 | }, 168 | onclick: { 169 | enable: true, 170 | mode: 'push' 171 | }, 172 | resize: true 173 | }, 174 | modes: { 175 | grab: { 176 | distance: 200, 177 | line_linked: { 178 | opacity: 1 179 | } 180 | }, 181 | bubble: { 182 | distance: 200, 183 | size: 80, 184 | duration: 0.4 185 | }, 186 | repulse: { 187 | distance: 200, 188 | duration: 0.4 189 | }, 190 | push: { 191 | particles_nb: 4 192 | }, 193 | remove: { 194 | particles_nb: 2 195 | } 196 | }, 197 | mouse: {} 198 | }, 199 | retina_detect: true 200 | } 201 | }; 202 | 203 | 204 | export function loadImg(params: IParams, tmp: ITmpParams) { 205 | let { particles } = params; 206 | 207 | tmp.img_error = undefined; 208 | 209 | if (particles.shape.type == 'image' && particles.shape.image.src != '') { 210 | if (tmp.img_type == 'svg') { 211 | let xhr: XMLHttpRequest = new XMLHttpRequest(); 212 | xhr.open('GET', particles.shape.image.src); 213 | xhr.onreadystatechange = (data: any) => { 214 | if (xhr.readyState == 4) { 215 | if (xhr.status == 200) { 216 | tmp.source_svg = data.currentTarget.response; 217 | if (tmp.source_svg == undefined) { 218 | let check: any; 219 | tmp.checkAnimFrame = requestAnimationFrame(check); 220 | } 221 | } else { 222 | tmp.img_error = true; 223 | throw "Error : image not found"; 224 | } 225 | } 226 | }; 227 | xhr.send(); 228 | } else { 229 | let img: HTMLImageElement = new Image(); 230 | img.addEventListener('load', () => { 231 | tmp.img_obj = img; 232 | cancelAnimationFrame(tmp.checkAnimFrame); 233 | }); 234 | img.src = particles.shape.image.src; 235 | } 236 | } else { 237 | tmp.img_error = true; 238 | throw "Error : no image.src"; 239 | } 240 | } 241 | 242 | export function createSvgImg(particle: Particle, tmp: ITmpParams): void { 243 | let svgXml: string = tmp.source_svg; 244 | let rgbHex: RegExp = /#([0-9A-F]{3,6})/gi; 245 | let coloredSvgXml: string = svgXml.replace(rgbHex, (m, r, g, b) => { 246 | let color_value: string; 247 | if (particle.color.rgb) { 248 | let { r, g, b } = particle.color.rgb; 249 | color_value = `rgba( ${r}, ${g}, ${b}, ${particle.opacity} )`; 250 | } else { 251 | let { h, s, l } = particle.color.hsl; 252 | color_value = `rgba( ${h}, ${s}, ${l}, ${particle.opacity} )`; 253 | } 254 | return color_value; 255 | }); 256 | 257 | let svg: Blob = new Blob([coloredSvgXml], { 258 | type: 'image/svg+xml;charset=utf-8' 259 | }); 260 | 261 | let DOMURL: any = window.URL || window; 262 | let url: any = DOMURL.createObjectURL(svg); 263 | 264 | let img = new Image(); 265 | img.addEventListener('load', () => { 266 | particle.img.obj = img; 267 | particle.img.loaded = true; 268 | DOMURL.revokeObjectURL(url); 269 | tmp.count_svg++; 270 | }); 271 | img.src = url; 272 | } -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-particle", 3 | "version": "1.0.4", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/ryuKKu-/angular-particle" 7 | }, 8 | "author": { 9 | "name": "Luc Raymond", 10 | "email": "ryukku.raymond@gmail.com" 11 | }, 12 | "keywords": [ 13 | "angular2", 14 | "angular4", 15 | "angular", 16 | "particlesjs", 17 | "particle", 18 | "typescript", 19 | "ng2", 20 | "particles" 21 | ], 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/ryuKKu-/angular-particle/issues" 25 | }, 26 | "module": "angular-particle.js", 27 | "typings": "angular-particle.d.ts", 28 | "peerDependencies": { 29 | "@angular/core": "^4.0.0", 30 | "rxjs": "^5.1.0", 31 | "zone.js": "^0.8.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/particles.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { IParams } from './lib/index'; 3 | 4 | @Component({ 5 | selector: 'particles', 6 | template: ` 7 |
8 | 9 |
10 | ` 11 | }) 12 | export class ParticlesComponent { 13 | 14 | @Input() width: number = 100; 15 | @Input() height: number = 100; 16 | @Input() params: IParams; 17 | @Input() style: Object = {}; 18 | 19 | constructor() { } 20 | } -------------------------------------------------------------------------------- /src/particles.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, AfterViewInit, HostListener, Input, OnDestroy } from "@angular/core"; 2 | import { CanvasManager, ICanvasParams, IParams, ITmpParams, getDefaultParams, isInArray, deepExtend, loadImg } from './lib/index'; 3 | 4 | @Directive({ 5 | selector: '[d-particles]' 6 | }) 7 | export class ParticlesDirective implements AfterViewInit, OnDestroy { 8 | @Input() set params(value: IParams) { 9 | let defaultParams: IParams = getDefaultParams(); 10 | this._params = deepExtend(defaultParams, value); 11 | } 12 | 13 | constructor(private el: ElementRef) { } 14 | 15 | private _canvasParams: ICanvasParams; 16 | private _params: IParams; 17 | private _tmpParams: ITmpParams = {}; 18 | private _canvasManager: CanvasManager; 19 | 20 | ngOnDestroy(): void { 21 | if (!this._canvasManager) { 22 | return; 23 | } 24 | this._canvasManager.cancelAnimation(); 25 | } 26 | 27 | ngAfterViewInit(): void { 28 | this._canvasParams = { 29 | el: this.el.nativeElement, 30 | ctx: this.el.nativeElement.getContext('2d'), 31 | width: this.el.nativeElement.offsetWidth, 32 | height: this.el.nativeElement.offsetHeight 33 | }; 34 | 35 | this._tmpParams.obj = { 36 | size_value: this._params.particles.size.value, 37 | size_anim_speed: this._params.particles.size.anim.speed, 38 | move_speed: this._params.particles.move.speed, 39 | line_linked_distance: this._params.particles.line_linked.distance, 40 | line_linked_width: this._params.particles.line_linked.width, 41 | mode_grab_distance: this._params.interactivity.modes.grab.distance, 42 | mode_bubble_distance: this._params.interactivity.modes.bubble.distance, 43 | mode_bubble_size: this._params.interactivity.modes.bubble.size, 44 | mode_repulse_distance: this._params.interactivity.modes.repulse.distance 45 | }; 46 | 47 | this._params.interactivity.el = (this._params.interactivity.detect_on == 'window') ? window : this._canvasParams.el; 48 | 49 | if (isInArray('image', this._params.particles.shape.type)) { 50 | this._tmpParams.img_type = this._params.particles.shape.image.src.substr(this._params.particles.shape.image.src.length - 3); 51 | loadImg(this._params, this._tmpParams); 52 | } 53 | 54 | this._canvasManager = new CanvasManager(this._canvasParams, this._params, this._tmpParams); 55 | this._canvasManager.draw(); 56 | } 57 | 58 | /** 59 | * Mouse move event 60 | * @param event 61 | */ 62 | @HostListener('mousemove', ['$event']) onMouseMove(event) { 63 | let { interactivity } = this._params; 64 | 65 | if (interactivity.events.onhover.enable || 66 | interactivity.events.onclick.enable) { 67 | 68 | let pos: { 69 | x: number; 70 | y: number; 71 | }; 72 | 73 | if (interactivity.el == window) { 74 | pos = { 75 | x: event.clientX, 76 | y: event.clientY 77 | }; 78 | } else { 79 | pos = { 80 | x: event.offsetX || event.clientX, 81 | y: event.offsetY || event.clientY 82 | }; 83 | } 84 | 85 | interactivity.mouse.pos_x = pos.x; 86 | interactivity.mouse.pos_y = pos.y; 87 | 88 | if (this._tmpParams.retina) { 89 | interactivity.mouse.pos_x *= this._canvasParams.pxratio; 90 | interactivity.mouse.pos_y *= this._canvasParams.pxratio; 91 | } 92 | 93 | interactivity.status = 'mousemove'; 94 | } 95 | } 96 | 97 | /** 98 | * Mouse leave event 99 | */ 100 | @HostListener('mouseleave') onMouseLeave() { 101 | let { interactivity } = this._params; 102 | 103 | if (interactivity.events.onhover.enable || 104 | interactivity.events.onclick.enable) { 105 | 106 | interactivity.mouse.pos_x = null; 107 | interactivity.mouse.pos_y = null; 108 | interactivity.status = 'mouseleave'; 109 | } 110 | } 111 | 112 | /** 113 | * Click event 114 | */ 115 | @HostListener('click') onClick() { 116 | let { interactivity, particles } = this._params; 117 | 118 | if (interactivity.events.onclick.enable) { 119 | interactivity.mouse.click_pos_x = interactivity.mouse.pos_x; 120 | interactivity.mouse.click_pos_y = interactivity.mouse.pos_y; 121 | interactivity.mouse.click_time = new Date().getTime(); 122 | 123 | switch (interactivity.events.onclick.mode) { 124 | case 'push': 125 | if (particles.move.enable) { 126 | this._canvasManager.particlesManager.pushParticles(interactivity.modes.push.particles_nb, interactivity.mouse); 127 | } else { 128 | if (interactivity.modes.push.particles_nb == 1) { 129 | this._canvasManager.particlesManager.pushParticles(interactivity.modes.push.particles_nb, interactivity.mouse); 130 | } else if (interactivity.modes.push.particles_nb > 1) { 131 | this._canvasManager.particlesManager.pushParticles(interactivity.modes.push.particles_nb); 132 | } 133 | } 134 | break; 135 | 136 | case 'remove': 137 | this._canvasManager.particlesManager.removeParticles(interactivity.modes.remove.particles_nb); 138 | break; 139 | 140 | case 'bubble': 141 | this._tmpParams.bubble_clicking = true; 142 | break; 143 | 144 | case 'repulse': 145 | this._tmpParams.repulse_clicking = true; 146 | this._tmpParams.repulse_count = 0; 147 | this._tmpParams.repulse_finish = false; 148 | setTimeout(() => { 149 | this._tmpParams.repulse_clicking = false; 150 | }, interactivity.modes.repulse.duration * 1000); 151 | break; 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "es2015", 5 | "target": "es5", 6 | "baseUrl": ".", 7 | "stripInternal": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "node", 11 | "outDir": "../build", 12 | "rootDir": ".", 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "skipLibCheck": true, 18 | "types": [] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "strictMetadataEmit": true, 23 | "skipTemplateCodegen": true, 24 | "flatModuleOutFile": "angular-particle.js", 25 | "flatModuleId": "angular-particle" 26 | }, 27 | "files": [ 28 | "./index.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tools/gulp/inline-resources.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // https://github.com/filipesilva/angular-quickstart-lib/blob/master/inline-resources.js 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const glob = require('glob'); 8 | const sass = require('node-sass'); 9 | const tildeImporter = require('node-sass-tilde-importer'); 10 | 11 | /** 12 | * Simple Promiseify function that takes a Node API and return a version that supports promises. 13 | * We use promises instead of synchronized functions to make the process less I/O bound and 14 | * faster. It also simplifies the code. 15 | */ 16 | function promiseify(fn) { 17 | return function () { 18 | const args = [].slice.call(arguments, 0); 19 | return new Promise((resolve, reject) => { 20 | fn.apply(this, args.concat([function (err, value) { 21 | if (err) { 22 | reject(err); 23 | } else { 24 | resolve(value); 25 | } 26 | }])); 27 | }); 28 | }; 29 | } 30 | 31 | const readFile = promiseify(fs.readFile); 32 | const writeFile = promiseify(fs.writeFile); 33 | 34 | /** 35 | * Inline resources in a tsc/ngc compilation. 36 | * @param projectPath {string} Path to the project. 37 | */ 38 | function inlineResources(projectPath) { 39 | 40 | // Match only TypeScript files in projectPath. 41 | const files = glob.sync('**/*.ts', {cwd: projectPath}); 42 | 43 | // For each file, inline the templates and styles under it and write the new file. 44 | return Promise.all(files.map(filePath => { 45 | const fullFilePath = path.join(projectPath, filePath); 46 | return readFile(fullFilePath, 'utf-8') 47 | .then(content => inlineResourcesFromString(content, url => { 48 | // Resolve the template url. 49 | return path.join(path.dirname(fullFilePath), url); 50 | })) 51 | .then(content => writeFile(fullFilePath, content)) 52 | .catch(err => { 53 | console.error('An error occured: ', err); 54 | }); 55 | })); 56 | } 57 | 58 | /** 59 | * Inline resources from a string content. 60 | * @param content {string} The source file's content. 61 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 62 | * @returns {string} The content with resources inlined. 63 | */ 64 | function inlineResourcesFromString(content, urlResolver) { 65 | // Curry through the inlining functions. 66 | return [ 67 | inlineTemplate, 68 | inlineStyle, 69 | removeModuleId 70 | ].reduce((content, fn) => fn(content, urlResolver), content); 71 | } 72 | 73 | /** 74 | * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and 75 | * replace with `template: ...` (with the content of the file included). 76 | * @param content {string} The source file's content. 77 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 78 | * @return {string} The content with all templates inlined. 79 | */ 80 | function inlineTemplate(content, urlResolver) { 81 | return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (m, templateUrl) { 82 | const templateFile = urlResolver(templateUrl); 83 | const templateContent = fs.readFileSync(templateFile, 'utf-8'); 84 | const shortenedTemplate = templateContent 85 | .replace(/([\n\r]\s*)+/gm, ' ') 86 | .replace(/"/g, '\\"'); 87 | return `template: "${shortenedTemplate}"`; 88 | }); 89 | } 90 | 91 | 92 | /** 93 | * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and 94 | * replace with `styles: [...]` (with the content of the file included). 95 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 96 | * @param content {string} The source file's content. 97 | * @return {string} The content with all styles inlined. 98 | */ 99 | function inlineStyle(content, urlResolver) { 100 | return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function (m, styleUrls) { 101 | const urls = eval(styleUrls); 102 | return 'styles: [' 103 | + urls.map(styleUrl => { 104 | const styleFile = urlResolver(styleUrl); 105 | const originContent = fs.readFileSync(styleFile, 'utf-8'); 106 | const styleContent = styleFile.endsWith('.scss') ? buildSass(originContent, styleFile) : originContent; 107 | const shortenedStyle = styleContent 108 | .replace(/([\n\r]\s*)+/gm, ' ') 109 | .replace(/"/g, '\\"'); 110 | return `"${shortenedStyle}"`; 111 | }) 112 | .join(',\n') 113 | + ']'; 114 | }); 115 | } 116 | 117 | /** 118 | * build sass content to css 119 | * @param content {string} the css content 120 | * @param sourceFile {string} the scss file sourceFile 121 | * @return {string} the generated css, empty string if error occured 122 | */ 123 | function buildSass(content, sourceFile) { 124 | try { 125 | const result = sass.renderSync({ 126 | data: content, 127 | file: sourceFile, 128 | importer: tildeImporter 129 | }); 130 | return result.css.toString() 131 | } catch (e) { 132 | console.error('\x1b[41m'); 133 | console.error('at ' + sourceFile + ':' + e.line + ":" + e.column); 134 | console.error(e.formatted); 135 | console.error('\x1b[0m'); 136 | return ""; 137 | } 138 | } 139 | 140 | /** 141 | * Remove every mention of `moduleId: module.id`. 142 | * @param content {string} The source file's content. 143 | * @returns {string} The content with all moduleId: mentions removed. 144 | */ 145 | function removeModuleId(content) { 146 | return content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, ''); 147 | } 148 | 149 | module.exports = inlineResources; 150 | module.exports.inlineResourcesFromString = inlineResourcesFromString; 151 | 152 | // Run inlineResources if module is being called directly from the CLI with arguments. 153 | if (require.main === module && process.argv.length > 2) { 154 | console.log('Inlining resources from project:', process.argv[2]); 155 | return inlineResources(process.argv[2]); 156 | } 157 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "experimentalDecorators": true, 5 | "moduleResolution": "node", 6 | "rootDir": "./src", 7 | "lib": [ 8 | "es2015", 9 | "dom" 10 | ], 11 | "skipLibCheck": true, 12 | "types": [] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "max-line-length": [ 20 | true, 21 | 140 22 | ], 23 | "member-access": false, 24 | "member-ordering": [ 25 | true, 26 | "static-before-instance", 27 | "variables-before-functions" 28 | ], 29 | "no-arg": true, 30 | "no-bitwise": true, 31 | "no-console": [ 32 | true, 33 | "debug", 34 | "info", 35 | "time", 36 | "timeEnd", 37 | "trace" 38 | ], 39 | "no-construct": true, 40 | "no-debugger": true, 41 | "no-duplicate-variable": true, 42 | "no-empty": false, 43 | "no-eval": true, 44 | "no-inferrable-types": true, 45 | "no-shadowed-variable": true, 46 | "no-string-literal": false, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unused-expression": true, 50 | "no-unused-variable": true, 51 | "no-use-before-declare": true, 52 | "no-var-keyword": true, 53 | "object-literal-sort-keys": false, 54 | "one-line": [ 55 | true, 56 | "check-open-brace", 57 | "check-catch", 58 | "check-else", 59 | "check-whitespace" 60 | ], 61 | "quotemark": [ 62 | true, 63 | "single" 64 | ], 65 | "radix": true, 66 | "semicolon": [ 67 | "always" 68 | ], 69 | "triple-equals": [ 70 | true, 71 | "allow-null-check" 72 | ], 73 | "typedef-whitespace": [ 74 | true, 75 | { 76 | "call-signature": "nospace", 77 | "index-signature": "nospace", 78 | "parameter": "nospace", 79 | "property-declaration": "nospace", 80 | "variable-declaration": "nospace" 81 | } 82 | ], 83 | "variable-name": false, 84 | "whitespace": [ 85 | true, 86 | "check-branch", 87 | "check-decl", 88 | "check-operator", 89 | "check-separator", 90 | "check-type" 91 | ], 92 | "directive-selector": [true, "attribute", "", "camelCase"], 93 | "component-selector": [true, "element", "", "kebab-case"], 94 | "use-input-property-decorator": true, 95 | "use-output-property-decorator": true, 96 | "use-host-property-decorator": true, 97 | "no-input-rename": true, 98 | "no-output-rename": true, 99 | "use-life-cycle-interface": true, 100 | "use-pipe-transform-interface": true, 101 | "component-class-suffix": true, 102 | "directive-class-suffix": true 103 | } 104 | } 105 | --------------------------------------------------------------------------------