├── .gitignore
├── README.md
├── config
├── common.js
├── dev.js
└── prod.js
├── cover.png
├── package-lock.json
├── package.json
├── src
├── index.html
├── scripts
│ ├── App.js
│ ├── gui
│ │ └── GUIView.js
│ ├── index.js
│ ├── utils
│ │ ├── easing.utils.js
│ │ ├── event.utils.js
│ │ ├── fetch.utils.js
│ │ └── math.utils.js
│ └── webgl
│ │ ├── WebGLView.js
│ │ ├── controls
│ │ └── InteractiveControls.js
│ │ └── particles
│ │ ├── Particles.js
│ │ └── TouchTexture.js
└── shaders
│ ├── particle.frag
│ └── particle.vert
└── static
├── css
├── base.css
└── demo1.css
└── images
├── 1.png
├── 2.png
├── avatar.jpg
├── sample-01.png
├── sample-02.png
├── sample-03.png
├── sample-04.png
└── skills
├── D3.png
├── antd.webp
├── aws.png
├── azure.png
├── babylon.png
├── binanceCoin.jpg
├── bitcoin.png
├── blockchain.jpg
├── bootstrap.png
├── chartjs.svg
├── ci.svg
├── cloudmongo.png
├── csharp.svg
├── css3.svg
├── django.png
├── docker.png
├── ethereum.png
├── express.png
├── firebase.png
├── flask.png
├── git.png
├── github.png
├── gitlab.png
├── googlecloud.svg
├── graphql.png
├── graphql.svg
├── html.png
├── jasmine.png
├── jquery.png
├── js.jpg
├── laravel.jpg
├── linux.png
├── litecoin.jpg
├── macox.png
├── materialUI.png
├── mocha.svg
├── mongodb.jpg
├── mysql.webp
├── next.jpg
├── node.jpg
├── php.png
├── postgresql.png
├── postman.png
├── python.png
├── react.png
├── reactNative.png
├── redux.png
├── sass.png
├── selenium.png
├── sqlite.png
├── tailwind.png
├── three.png
├── ts.png
├── vue.png
├── webRTC.png
├── webgl.png
└── windows.png
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Interactive Particles with Three.js
2 |
3 | A tutorial demonstrating how to draw a large number of particles with Three.js and an efficient way to make them react to mouse and touch input using an off-screen texture.
4 |
5 | 
6 |
7 |
8 | ## Run
9 | - Install `npm install`
10 | - Run `npm start`
11 | - Build `npm run build`
12 |
13 | ## Libraries
14 | - [ControlKit](https://github.com/brunoimbrizi/controlkit.js) - GUI
15 | - [gsap](https://www.npmjs.com/package/gsap) - animation platform
16 | - [glslify](https://github.com/glslify/glslify) - module system for GLSL
17 | - [stats.js](https://github.com/mrdoob/stats.js/) - performance monitor
18 | - [Three.js](https://github.com/mrdoob/three.js/) - WebGL library
19 |
--------------------------------------------------------------------------------
/config/common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const CleanWebpackPlugin = require('clean-webpack-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin');
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 |
7 | const __root = path.resolve(__dirname, '../');
8 |
9 | module.exports = {
10 | entry: {
11 | index: ['@babel/polyfill', './src/scripts/index.js'],
12 | },
13 | output: {
14 | path: path.resolve(__root, 'dist'),
15 | filename: 'scripts/[name].[chunkhash].js',
16 | chunkFilename: 'scripts/[name].[chunkhash].js',
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.js$/,
22 | use: {
23 | loader: 'babel-loader',
24 | options: {
25 | presets: ['@babel/preset-env'],
26 | plugins: ['@babel/plugin-syntax-dynamic-import']
27 | }
28 | },
29 | exclude: /node_modules/
30 | },
31 | {
32 | test: /\.(glsl|frag|vert)$/,
33 | use: ['glslify-import-loader', 'raw-loader', 'glslify-loader']
34 | },
35 | {
36 | test: /three\/examples\/js/,
37 | use: 'imports-loader?THREE=three'
38 | },
39 | /*
40 | {
41 | test: /\.css$/,
42 | use: ['style-loader', 'css-loader']
43 | },
44 | {
45 | test: /\.(woff|woff2|eot|ttf|otf)$/,
46 | use: 'file-loader'
47 | },
48 | {
49 | test: /\.(jpe?g|png|gif)$/i,
50 | use: 'file-loader'
51 | }
52 | */
53 | ]
54 | },
55 | resolve: {
56 | alias: {
57 | 'three-examples': path.join(__root, './node_modules/three/examples/js'),
58 | }
59 | },
60 | plugins: [
61 | new CleanWebpackPlugin(
62 | ['dist'],
63 | { root: __root },
64 | ),
65 | new CopyWebpackPlugin([
66 | {
67 | from: path.resolve(__root, 'static'),
68 | }
69 | ]),
70 | new HtmlWebpackPlugin({
71 | template: './src/index.html',
72 | }),
73 | new webpack.ProvidePlugin({
74 | 'THREE': 'three'
75 | })
76 | ]
77 | };
78 |
--------------------------------------------------------------------------------
/config/dev.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'development',
6 | devtool: 'inline-source-map',
7 | devServer: {
8 | contentBase: './dist',
9 | host: '0.0.0.0'
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/config/prod.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'production'
6 | });
7 |
--------------------------------------------------------------------------------
/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/cover.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webpack-threejs",
3 | "version": "1.0.1",
4 | "description": "A simple boilerplate to start projects with Three.js using webpack.",
5 | "keywords": [
6 | "three.js",
7 | "webpack",
8 | "glsl",
9 | "glslify"
10 | ],
11 | "author": {
12 | "name": "Bruno Imbrizi",
13 | "url": "http://brunoimbrizi.com"
14 | },
15 | "license": "MIT",
16 | "sideEffects": false,
17 | "scripts": {
18 | "build": "webpack -p --config config/prod.js",
19 | "start": "webpack-dev-server --config config/dev.js"
20 | },
21 | "dependencies": {
22 | "@brunoimbrizi/controlkit": "^0.1.93",
23 | "browser-detect": "^0.2.28",
24 | "domready": "^1.0.8",
25 | "glsl-noise": "0.0.0",
26 | "gsap": "^2.0.2",
27 | "stats.js": "^0.17.0",
28 | "three": "^0.98.0"
29 | },
30 | "devDependencies": {
31 | "@babel/core": "^7.0.0-beta.52",
32 | "@babel/plugin-syntax-dynamic-import": "^7.0.0-rc.1",
33 | "@babel/polyfill": "^7.0.0-beta.55",
34 | "@babel/preset-env": "^7.0.0-beta.52",
35 | "babel-loader": "^8.0.0-beta.4",
36 | "clean-webpack-plugin": "^0.1.19",
37 | "copy-webpack-plugin": "^4.5.2",
38 | "css-loader": "^0.28.11",
39 | "file-loader": "^1.1.11",
40 | "glslify": "^6.2.1",
41 | "glslify-import-loader": "^0.1.2",
42 | "glslify-loader": "^1.0.2",
43 | "html-webpack-plugin": "^3.2.0",
44 | "js-yaml": ">=3.13.1",
45 | "imports-loader": "^0.8.0",
46 | "raw-loader": "^0.5.1",
47 | "style-loader": "^0.21.0",
48 | "webpack": "^4.15.1",
49 | "webpack-cli": "^3.0.8",
50 | "webpack-dev-server": ">=3.1.11",
51 | "webpack-merge": "^4.1.3"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Interactive Particles
7 |
8 |
9 |
10 |
11 |
12 |
13 |
25 |
26 |
27 |
28 |
29 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/scripts/App.js:
--------------------------------------------------------------------------------
1 | import WebGLView from './webgl/WebGLView';
2 | import GUIView from './gui/GUIView';
3 |
4 | export default class App {
5 |
6 | constructor() {
7 |
8 | }
9 |
10 | init() {
11 | this.initWebGL();
12 | this.initGUI();
13 | this.addListeners();
14 | this.animate();
15 | this.resize();
16 | }
17 |
18 | initWebGL() {
19 | this.webgl = new WebGLView(this);
20 | document.querySelector('.container').appendChild(this.webgl.renderer.domElement);
21 | }
22 |
23 | initGUI() {
24 | this.gui = new GUIView(this);
25 | }
26 |
27 | addListeners() {
28 | this.handlerAnimate = this.animate.bind(this);
29 |
30 | window.addEventListener('resize', this.resize.bind(this));
31 | window.addEventListener('keyup', this.keyup.bind(this));
32 |
33 | const el = this.webgl.renderer.domElement;
34 | el.addEventListener('click', this.click.bind(this));
35 | }
36 |
37 | animate() {
38 | this.update();
39 | this.draw();
40 |
41 | this.raf = requestAnimationFrame(this.handlerAnimate);
42 | }
43 |
44 | // ---------------------------------------------------------------------------------------------
45 | // PUBLIC
46 | // ---------------------------------------------------------------------------------------------
47 |
48 | update() {
49 | if (this.gui.stats) this.gui.stats.begin();
50 | if (this.webgl) this.webgl.update();
51 | if (this.gui) this.gui.update();
52 | }
53 |
54 | draw() {
55 | if (this.webgl) this.webgl.draw();
56 | if (this.gui.stats) this.gui.stats.end();
57 | }
58 |
59 | // ---------------------------------------------------------------------------------------------
60 | // EVENT HANDLERS
61 | // ---------------------------------------------------------------------------------------------
62 |
63 | resize() {
64 | if (this.webgl) this.webgl.resize();
65 | }
66 |
67 | keyup(e) {
68 | // g
69 | if (e.keyCode == 71) { if (this.gui) this.gui.toggle(); }
70 | }
71 |
72 | click(e) {
73 | this.webgl.next();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/scripts/gui/GUIView.js:
--------------------------------------------------------------------------------
1 | import ControlKit from '@brunoimbrizi/controlkit';
2 | import Stats from 'stats.js';
3 |
4 | export default class GUIView {
5 |
6 | constructor(app) {
7 | this.app = app;
8 |
9 | this.particlesHitArea = false;
10 | this.particlesRandom = 2;
11 | this.particlesDepth = 4;
12 | this.particlesSize = 1.5;
13 |
14 | this.touchRadius = 0.15;
15 |
16 | this.range = [0, 1];
17 | this.rangeRandom = [1, 10];
18 | this.rangeSize = [0, 3];
19 | this.rangeDepth = [1, 10];
20 | this.rangeRadius = [0, 0.5];
21 |
22 | this.initControlKit();
23 | // this.initStats();
24 |
25 | // this.disable();
26 | }
27 |
28 | initControlKit() {
29 | this.controlKit = new ControlKit();
30 | this.controlKit.addPanel({ width: 300, enable: false })
31 |
32 | .addGroup({label: 'Touch', enable: true })
33 | .addCanvas({ label: 'trail', height: 64 })
34 | .addSlider(this, 'touchRadius', 'rangeRadius', { label: 'radius', onChange: this.onTouchChange.bind(this) })
35 |
36 | .addGroup({label: 'Particles', enable: true })
37 | // .addCheckbox(this, 'particlesHitArea', { label: 'hit area', onChange: this.onParticlesChange.bind(this) })
38 | .addSlider(this, 'particlesRandom', 'rangeRandom', { label: 'random', onChange: this.onParticlesChange.bind(this) })
39 | .addSlider(this, 'particlesDepth', 'rangeDepth', { label: 'depth', onChange: this.onParticlesChange.bind(this) })
40 | .addSlider(this, 'particlesSize', 'rangeSize', { label: 'size', onChange: this.onParticlesChange.bind(this) })
41 |
42 | // store reference to canvas
43 | const component = this.controlKit.getComponentBy({ label: 'trail' });
44 | if (!component) return;
45 |
46 | this.touchCanvas = component._canvas;
47 | this.touchCtx = this.touchCanvas.getContext('2d');
48 | }
49 |
50 | initStats() {
51 | this.stats = new Stats();
52 |
53 | document.body.appendChild(this.stats.dom);
54 | }
55 |
56 | // ---------------------------------------------------------------------------------------------
57 | // PUBLIC
58 | // ---------------------------------------------------------------------------------------------
59 |
60 | update() {
61 | // draw touch texture
62 | if (this.touchCanvas) {
63 | if (!this.app.webgl) return;
64 | if (!this.app.webgl.particles) return;
65 | if (!this.app.webgl.particles.touch) return;
66 | const source = this.app.webgl.particles.touch.canvas;
67 | const x = Math.floor((this.touchCanvas.width - source.width) * 0.5);
68 | this.touchCtx.fillRect(0, 0, this.touchCanvas.width, this.touchCanvas.height);
69 | this.touchCtx.drawImage(source, x, 0);
70 | }
71 | }
72 |
73 | enable() {
74 | this.controlKit.enable();
75 | if (this.stats) this.stats.dom.style.display = '';
76 | }
77 |
78 | disable() {
79 | this.controlKit.disable();
80 | if (this.stats) this.stats.dom.style.display = 'none';
81 | }
82 |
83 | toggle() {
84 | if (this.controlKit._enabled) this.disable();
85 | else this.enable();
86 | }
87 |
88 | onTouchChange() {
89 | if (!this.app.webgl) return;
90 | if (!this.app.webgl.particles) return;
91 |
92 | this.app.webgl.particles.touch.radius = this.touchRadius;
93 | }
94 |
95 | onParticlesChange() {
96 | if (!this.app.webgl) return;
97 | if (!this.app.webgl.particles) return;
98 |
99 | this.app.webgl.particles.object3D.material.uniforms.uRandom.value = this.particlesRandom;
100 | this.app.webgl.particles.object3D.material.uniforms.uDepth.value = this.particlesDepth;
101 | this.app.webgl.particles.object3D.material.uniforms.uSize.value = this.particlesSize;
102 |
103 | this.app.webgl.particles.hitArea.material.visible = this.particlesHitArea;
104 | }
105 |
106 | onPostProcessingChange() {
107 | if (!this.app.webgl.composer) return;
108 | this.app.webgl.composer.enabled = this.postProcessing;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/scripts/index.js:
--------------------------------------------------------------------------------
1 | import ready from 'domready';
2 |
3 | import App from './App';
4 |
5 | ready(() => {
6 | window.app = new App();
7 | window.app.init();
8 | });
9 |
--------------------------------------------------------------------------------
/src/scripts/utils/easing.utils.js:
--------------------------------------------------------------------------------
1 | const easeInQuad = (t, b, c, d) => {
2 | t /= d;
3 | return c * t * t + b;
4 | };
5 |
6 | const easeOutQuad = (t, b, c, d) => {
7 | t /= d;
8 | return -c * t * (t - 2) + b;
9 | };
10 |
11 | const easeInOutQuad = (t, b, c, d) => {
12 | t /= d / 2;
13 | if (t < 1) return c / 2 * t * t + b;
14 | t--;
15 | return -c/2 * (t * (t - 2) - 1) + b;
16 | };
17 |
18 | const easeInOutQuart = (t, b, c, d) => {
19 | if ((t /= d / 2) < 1) {
20 | return c / 2 * t * t * t * t + b;
21 | } else {
22 | return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
23 | }
24 | };
25 |
26 | const easeInSine = (t, b, c, d) => {
27 | return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
28 | };
29 |
30 | const easeOutSine = (t, b, c, d) => {
31 | return c * Math.sin(t/d * (Math.PI/2)) + b;
32 | };
33 |
34 | const easeInOutSine = (t, b, c, d) => {
35 | return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
36 | };
37 |
38 | export { easeInQuad, easeOutQuad, easeInOutQuad, easeInOutQuart, easeInSine, easeOutSine, easeInOutSine };
--------------------------------------------------------------------------------
/src/scripts/utils/event.utils.js:
--------------------------------------------------------------------------------
1 | let alreadyTested = false;
2 | let passiveSupported = false;
3 |
4 | const isSupported = () => {
5 | if (alreadyTested) return passiveSupported;
6 | alreadyTested = true;
7 |
8 | // Test via a getter in the options object to see if the passive property is accessed
9 | try {
10 | let opts = Object.defineProperty({}, 'passive', {
11 | get: () => {
12 | passiveSupported = true;
13 | }
14 | });
15 | window.addEventListener('test', null, opts);
16 | } catch (e) {
17 | return passiveSupported;
18 | }
19 | window.removeEventListener('test', null, opts);
20 | return passiveSupported;
21 | };
22 |
23 | const passiveEvent = () => {
24 | return isSupported() ? { passive: true } : false;
25 | };
26 |
27 | export { passiveEvent };
--------------------------------------------------------------------------------
/src/scripts/utils/fetch.utils.js:
--------------------------------------------------------------------------------
1 | const fetchJSON = async (uri) => {
2 | return await (await fetch(uri)).json();
3 | };
4 |
5 | export { fetchJSON };
--------------------------------------------------------------------------------
/src/scripts/utils/math.utils.js:
--------------------------------------------------------------------------------
1 | const PI = Math.PI;
2 | const HALF_PI = Math.PI / 2;
3 | const TWO_PI = Math.PI * 2;
4 | const QUARTER_PI = Math.PI / 4;
5 | const DEG_TO_RAD = Math.PI / 180;
6 | const RAD_TO_DEG = 180 / Math.PI;
7 |
8 | const clamp = (num, min, max) => {
9 | if (max < min) {
10 | const tmax = min;
11 | min = max;
12 | max = tmax;
13 | }
14 |
15 | if (num < min) return min;
16 | else if (num > max) return max;
17 |
18 | return num;
19 | };
20 |
21 | const lerp = (min, max, amount) => {
22 | return min + amount * (max - min);
23 | };
24 |
25 | const map = (num, min1, max1, min2, max2, round = false, constrainMin = true, constrainMax = true) => {
26 | if (constrainMin && num < min1) return min2;
27 | if (constrainMax && num > max1) return max2;
28 |
29 | const num1 = (num - min1) / (max1 - min1);
30 | const num2 = (num1 * (max2 - min2)) + min2;
31 | if (round) return Math.round(num2);
32 | return num2;
33 | };
34 |
35 | const mod = (n, m) => {
36 | return ((n % m) + m) % m;
37 | };
38 |
39 | const random = (min, max) => {
40 | if (Object.prototype.toString.call(min) === '[object Array]') return min[~~(Math.random() * min.length)];
41 |
42 | if (typeof max !== 'number') {
43 | max = min || 1;
44 | min = 0;
45 | }
46 |
47 | return min + Math.random() * (max - min);
48 | };
49 |
50 |
51 | export { PI, HALF_PI, QUARTER_PI, TWO_PI, DEG_TO_RAD, RAD_TO_DEG, clamp, lerp, map, mod, random };
52 |
--------------------------------------------------------------------------------
/src/scripts/webgl/WebGLView.js:
--------------------------------------------------------------------------------
1 | import 'three';
2 | import { TweenLite } from 'gsap/TweenMax';
3 |
4 | import InteractiveControls from './controls/InteractiveControls';
5 | import Particles from './particles/Particles';
6 |
7 | const glslify = require('glslify');
8 |
9 | export default class WebGLView {
10 |
11 | constructor(app) {
12 | this.app = app;
13 |
14 | this.samples = [
15 | 'images/avatar.jpg',
16 | 'images/2.png',
17 | 'images/1.png',
18 | ];
19 |
20 | this.initThree();
21 | this.initParticles();
22 | this.initControls();
23 |
24 | const rnd = ~~(Math.random() * this.samples.length);
25 | this.goto(rnd);
26 | }
27 |
28 | initThree() {
29 | // scene
30 | this.scene = new THREE.Scene();
31 |
32 | // camera
33 | this.camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000);
34 | this.camera.position.z = 300;
35 |
36 | // renderer
37 | this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
38 |
39 | // clock
40 | this.clock = new THREE.Clock(true);
41 | }
42 |
43 | initControls() {
44 | this.interactive = new InteractiveControls(this.camera, this.renderer.domElement);
45 | }
46 |
47 | initParticles() {
48 | this.particles = new Particles(this);
49 | this.scene.add(this.particles.container);
50 | }
51 |
52 | // ---------------------------------------------------------------------------------------------
53 | // PUBLIC
54 | // ---------------------------------------------------------------------------------------------
55 |
56 | update() {
57 | const delta = this.clock.getDelta();
58 |
59 | if (this.particles) this.particles.update(delta);
60 | }
61 |
62 | draw() {
63 | this.renderer.render(this.scene, this.camera);
64 | }
65 |
66 |
67 | goto(index) {
68 | // init next
69 | if (this.currSample == null) this.particles.init(this.samples[index]);
70 | // hide curr then init next
71 | else {
72 | this.particles.hide(true).then(() => {
73 | this.particles.init(this.samples[index]);
74 | });
75 | }
76 |
77 | this.currSample = index;
78 | }
79 |
80 | next() {
81 | if (this.currSample < this.samples.length - 1) this.goto(this.currSample + 1);
82 | else this.goto(0);
83 | }
84 |
85 | // ---------------------------------------------------------------------------------------------
86 | // EVENT HANDLERS
87 | // ---------------------------------------------------------------------------------------------
88 |
89 | resize() {
90 | if (!this.renderer) return;
91 | this.camera.aspect = window.innerWidth / window.innerHeight;
92 | this.camera.updateProjectionMatrix();
93 |
94 | this.fovHeight = 2 * Math.tan((this.camera.fov * Math.PI) / 180 / 2) * this.camera.position.z;
95 |
96 | this.renderer.setSize(window.innerWidth, window.innerHeight);
97 |
98 | if (this.interactive) this.interactive.resize();
99 | if (this.particles) this.particles.resize();
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/scripts/webgl/controls/InteractiveControls.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'events';
2 | import * as THREE from 'three';
3 | import browser from 'browser-detect';
4 |
5 | import { passiveEvent } from '../../utils/event.utils.js';
6 |
7 | export default class InteractiveControls extends EventEmitter {
8 |
9 | get enabled() { return this._enabled; }
10 |
11 | constructor(camera, el) {
12 | super();
13 |
14 | this.camera = camera;
15 | this.el = el || window;
16 |
17 | this.plane = new THREE.Plane();
18 | this.raycaster = new THREE.Raycaster();
19 |
20 | this.mouse = new THREE.Vector2();
21 | this.offset = new THREE.Vector3();
22 | this.intersection = new THREE.Vector3();
23 |
24 | this.objects = [];
25 | this.hovered = null;
26 | this.selected = null;
27 |
28 | this.isDown = false;
29 |
30 | this.browser = browser();
31 |
32 | this.enable();
33 | }
34 |
35 | enable() {
36 | if (this.enabled) return;
37 | this.addListeners();
38 | this._enabled = true;
39 | }
40 |
41 | disable() {
42 | if (!this.enabled) return;
43 | this.removeListeners();
44 | this._enabled = false;
45 | }
46 |
47 | addListeners() {
48 | this.handlerDown = this.onDown.bind(this);
49 | this.handlerMove = this.onMove.bind(this);
50 | this.handlerUp = this.onUp.bind(this);
51 | this.handlerLeave = this.onLeave.bind(this);
52 |
53 | if (this.browser.mobile) {
54 | this.el.addEventListener('touchstart', this.handlerDown, passiveEvent);
55 | this.el.addEventListener('touchmove', this.handlerMove, passiveEvent);
56 | this.el.addEventListener('touchend', this.handlerUp, passiveEvent);
57 | }
58 | else {
59 | this.el.addEventListener('mousedown', this.handlerDown);
60 | this.el.addEventListener('mousemove', this.handlerMove);
61 | this.el.addEventListener('mouseup', this.handlerUp);
62 | this.el.addEventListener('mouseleave', this.handlerLeave);
63 | }
64 | }
65 |
66 | removeListeners() {
67 | if (this.browser.mobile) {
68 | this.el.removeEventListener('touchstart', this.handlerDown);
69 | this.el.removeEventListener('touchmove', this.handlerMove);
70 | this.el.removeEventListener('touchend', this.handlerUp);
71 | }
72 | else {
73 | this.el.removeEventListener('mousedown', this.handlerDown);
74 | this.el.removeEventListener('mousemove', this.handlerMove);
75 | this.el.removeEventListener('mouseup', this.handlerUp);
76 | this.el.removeEventListener('mouseleave', this.handlerLeave);
77 | }
78 | }
79 |
80 | resize(x, y, width, height) {
81 | if (x || y || width || height) {
82 | this.rect = { x, y, width, height };
83 | }
84 | else if (this.el === window) {
85 | this.rect = { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight };
86 | }
87 | else {
88 | this.rect = this.el.getBoundingClientRect();
89 | }
90 | }
91 |
92 | onMove(e) {
93 | const t = (e.touches) ? e.touches[0] : e;
94 | const touch = { x: t.clientX, y: t.clientY };
95 |
96 | this.mouse.x = ((touch.x + this.rect.x) / this.rect.width) * 2 - 1;
97 | this.mouse.y = -((touch.y + this.rect.y) / this.rect.height) * 2 + 1;
98 |
99 | this.raycaster.setFromCamera(this.mouse, this.camera);
100 |
101 | /*
102 | // is dragging
103 | if (this.selected && this.isDown) {
104 | if (this.raycaster.ray.intersectPlane(this.plane, this.intersection)) {
105 | this.emit('interactive-drag', { object: this.selected, position: this.intersection.sub(this.offset) });
106 | }
107 | return;
108 | }
109 | */
110 |
111 | const intersects = this.raycaster.intersectObjects(this.objects);
112 |
113 | if (intersects.length > 0) {
114 | const object = intersects[0].object;
115 | this.intersectionData = intersects[0];
116 |
117 | this.plane.setFromNormalAndCoplanarPoint(this.camera.getWorldDirection(this.plane.normal), object.position);
118 |
119 | if (this.hovered !== object) {
120 | this.emit('interactive-out', { object: this.hovered });
121 | this.emit('interactive-over', { object });
122 | this.hovered = object;
123 | }
124 | else {
125 | this.emit('interactive-move', { object, intersectionData: this.intersectionData });
126 | }
127 | }
128 | else {
129 | this.intersectionData = null;
130 |
131 | if (this.hovered !== null) {
132 | this.emit('interactive-out', { object: this.hovered });
133 | this.hovered = null;
134 | }
135 | }
136 | }
137 |
138 | onDown(e) {
139 | this.isDown = true;
140 | this.onMove(e);
141 |
142 | this.emit('interactive-down', { object: this.hovered, previous: this.selected, intersectionData: this.intersectionData });
143 | this.selected = this.hovered;
144 |
145 | if (this.selected) {
146 | if (this.raycaster.ray.intersectPlane(this.plane, this.intersection)) {
147 | this.offset.copy(this.intersection).sub(this.selected.position);
148 | }
149 | }
150 | }
151 |
152 | onUp(e) {
153 | this.isDown = false;
154 |
155 | this.emit('interactive-up', { object: this.hovered });
156 | }
157 |
158 | onLeave(e) {
159 | this.onUp(e);
160 |
161 | this.emit('interactive-out', { object: this.hovered });
162 | this.hovered = null;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/scripts/webgl/particles/Particles.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | import TouchTexture from './TouchTexture';
4 |
5 | const glslify = require('glslify');
6 |
7 | export default class Particles {
8 |
9 | constructor(webgl) {
10 | this.webgl = webgl;
11 | this.container = new THREE.Object3D();
12 | }
13 |
14 | init(src) {
15 | const loader = new THREE.TextureLoader();
16 |
17 | loader.load(src, (texture) => {
18 | this.texture = texture;
19 | this.texture.minFilter = THREE.LinearFilter;
20 | this.texture.magFilter = THREE.LinearFilter;
21 | this.texture.format = THREE.RGBFormat;
22 |
23 | this.width = texture.image.width;
24 | this.height = texture.image.height;
25 |
26 | this.initPoints(true);
27 | this.initHitArea();
28 | this.initTouch();
29 | this.resize();
30 | this.show();
31 | });
32 | }
33 |
34 | initPoints(discard) {
35 | this.numPoints = this.width * this.height;
36 |
37 | let numVisible = this.numPoints;
38 | let threshold = 0;
39 | let originalColors;
40 |
41 | if (discard) {
42 | // discard pixels darker than threshold #22
43 | numVisible = 0;
44 | threshold = 34;
45 |
46 | const img = this.texture.image;
47 | const canvas = document.createElement('canvas');
48 | const ctx = canvas.getContext('2d');
49 |
50 | canvas.width = this.width;
51 | canvas.height = this.height;
52 | ctx.scale(1, -1);
53 | ctx.drawImage(img, 0, 0, this.width, this.height * -1);
54 |
55 | const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
56 | originalColors = Float32Array.from(imgData.data);
57 |
58 | for (let i = 0; i < this.numPoints; i++) {
59 | if (originalColors[i * 4 + 0] > threshold) numVisible++;
60 | }
61 |
62 | // console.log('numVisible', numVisible, this.numPoints);
63 | }
64 |
65 | const uniforms = {
66 | uTime: { value: 0 },
67 | uRandom: { value: 1.0 },
68 | uDepth: { value: 2.0 },
69 | uSize: { value: 0.0 },
70 | uTextureSize: { value: new THREE.Vector2(this.width, this.height) },
71 | uTexture: { value: this.texture },
72 | uTouch: { value: null },
73 | };
74 |
75 | const material = new THREE.RawShaderMaterial({
76 | uniforms,
77 | vertexShader: glslify(require('../../../shaders/particle.vert')),
78 | fragmentShader: glslify(require('../../../shaders/particle.frag')),
79 | depthTest: false,
80 | transparent: true,
81 | // blending: THREE.AdditiveBlending
82 | });
83 |
84 | const geometry = new THREE.InstancedBufferGeometry();
85 |
86 | // positions
87 | const positions = new THREE.BufferAttribute(new Float32Array(4 * 3), 3);
88 | positions.setXYZ(0, -0.5, 0.5, 0.0);
89 | positions.setXYZ(1, 0.5, 0.5, 0.0);
90 | positions.setXYZ(2, -0.5, -0.5, 0.0);
91 | positions.setXYZ(3, 0.5, -0.5, 0.0);
92 | geometry.addAttribute('position', positions);
93 |
94 | // uvs
95 | const uvs = new THREE.BufferAttribute(new Float32Array(4 * 2), 2);
96 | uvs.setXYZ(0, 0.0, 0.0);
97 | uvs.setXYZ(1, 1.0, 0.0);
98 | uvs.setXYZ(2, 0.0, 1.0);
99 | uvs.setXYZ(3, 1.0, 1.0);
100 | geometry.addAttribute('uv', uvs);
101 |
102 | // index
103 | geometry.setIndex(new THREE.BufferAttribute(new Uint16Array([ 0, 2, 1, 2, 3, 1 ]), 1));
104 |
105 | const indices = new Uint16Array(numVisible);
106 | const offsets = new Float32Array(numVisible * 3);
107 | const angles = new Float32Array(numVisible);
108 |
109 | for (let i = 0, j = 0; i < this.numPoints; i++) {
110 | if (discard && originalColors[i * 4 + 0] <= threshold) continue;
111 |
112 | offsets[j * 3 + 0] = i % this.width;
113 | offsets[j * 3 + 1] = Math.floor(i / this.width);
114 |
115 | indices[j] = i;
116 |
117 | angles[j] = Math.random() * Math.PI;
118 |
119 | j++;
120 | }
121 |
122 | geometry.addAttribute('pindex', new THREE.InstancedBufferAttribute(indices, 1, false));
123 | geometry.addAttribute('offset', new THREE.InstancedBufferAttribute(offsets, 3, false));
124 | geometry.addAttribute('angle', new THREE.InstancedBufferAttribute(angles, 1, false));
125 |
126 | this.object3D = new THREE.Mesh(geometry, material);
127 | this.container.add(this.object3D);
128 | }
129 |
130 | initTouch() {
131 | // create only once
132 | if (!this.touch) this.touch = new TouchTexture(this);
133 | this.object3D.material.uniforms.uTouch.value = this.touch.texture;
134 | }
135 |
136 | initHitArea() {
137 | const geometry = new THREE.PlaneGeometry(this.width, this.height, 1, 1);
138 | const material = new THREE.MeshBasicMaterial({ color: 0xFFFFFF, wireframe: true, depthTest: false });
139 | material.visible = false;
140 | this.hitArea = new THREE.Mesh(geometry, material);
141 | this.container.add(this.hitArea);
142 | }
143 |
144 | addListeners() {
145 | this.handlerInteractiveMove = this.onInteractiveMove.bind(this);
146 |
147 | this.webgl.interactive.addListener('interactive-move', this.handlerInteractiveMove);
148 | this.webgl.interactive.objects.push(this.hitArea);
149 | this.webgl.interactive.enable();
150 | }
151 |
152 | removeListeners() {
153 | this.webgl.interactive.removeListener('interactive-move', this.handlerInteractiveMove);
154 |
155 | const index = this.webgl.interactive.objects.findIndex(obj => obj === this.hitArea);
156 | this.webgl.interactive.objects.splice(index, 1);
157 | this.webgl.interactive.disable();
158 | }
159 |
160 | // ---------------------------------------------------------------------------------------------
161 | // PUBLIC
162 | // ---------------------------------------------------------------------------------------------
163 |
164 | update(delta) {
165 | if (!this.object3D) return;
166 | if (this.touch) this.touch.update();
167 |
168 | this.object3D.material.uniforms.uTime.value += delta;
169 | }
170 |
171 | show(time = 1.0) {
172 | // reset
173 | TweenLite.fromTo(this.object3D.material.uniforms.uSize, time, { value: 0.5 }, { value: 1.5 });
174 | TweenLite.to(this.object3D.material.uniforms.uRandom, time, { value: 2.0 });
175 | TweenLite.fromTo(this.object3D.material.uniforms.uDepth, time * 1.5, { value: 40.0 }, { value: 4.0 });
176 |
177 | this.addListeners();
178 | }
179 |
180 | hide(_destroy, time = 0.8) {
181 | return new Promise((resolve, reject) => {
182 | TweenLite.to(this.object3D.material.uniforms.uRandom, time, { value: 5.0, onComplete: () => {
183 | if (_destroy) this.destroy();
184 | resolve();
185 | } });
186 | TweenLite.to(this.object3D.material.uniforms.uDepth, time, { value: -20.0, ease: Quad.easeIn });
187 | TweenLite.to(this.object3D.material.uniforms.uSize, time * 0.8, { value: 0.0 });
188 |
189 | this.removeListeners();
190 | });
191 | }
192 |
193 | destroy() {
194 | if (!this.object3D) return;
195 |
196 | this.object3D.parent.remove(this.object3D);
197 | this.object3D.geometry.dispose();
198 | this.object3D.material.dispose();
199 | this.object3D = null;
200 |
201 | if (!this.hitArea) return;
202 |
203 | this.hitArea.parent.remove(this.hitArea);
204 | this.hitArea.geometry.dispose();
205 | this.hitArea.material.dispose();
206 | this.hitArea = null;
207 | }
208 |
209 | // ---------------------------------------------------------------------------------------------
210 | // EVENT HANDLERS
211 | // ---------------------------------------------------------------------------------------------
212 |
213 | resize() {
214 | if (!this.object3D) return;
215 |
216 | const scale = this.webgl.fovHeight / this.height;
217 | this.object3D.scale.set(scale, scale, 1);
218 | this.hitArea.scale.set(scale, scale, 1);
219 | }
220 |
221 | onInteractiveMove(e) {
222 | const uv = e.intersectionData.uv;
223 | if (this.touch) this.touch.addTouch(uv);
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/src/scripts/webgl/particles/TouchTexture.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | import { easeOutQuad, easeInOutQuad, easeOutSine, easeInOutSine } from '../../utils/easing.utils';
4 |
5 | export default class TouchTexture {
6 | constructor(parent) {
7 | this.parent = parent;
8 | this.size = 64;
9 | this.maxAge = 120;
10 | this.radius = 0.15;
11 | this.trail = [];
12 |
13 | this.initTexture();
14 | }
15 |
16 | initTexture() {
17 | this.canvas = document.createElement('canvas');
18 | this.canvas.width = this.canvas.height = this.size;
19 | this.ctx = this.canvas.getContext('2d');
20 | this.ctx.fillStyle = 'black';
21 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
22 |
23 | this.texture = new THREE.Texture(this.canvas);
24 |
25 | this.canvas.id = 'touchTexture';
26 | this.canvas.style.width = this.canvas.style.height = `${this.canvas.width}px`;
27 | }
28 |
29 | update(delta) {
30 | this.clear();
31 |
32 | // age points
33 | this.trail.forEach((point, i) => {
34 | point.age++;
35 | // remove old
36 | if (point.age > this.maxAge) {
37 | this.trail.splice(i, 1);
38 | }
39 | });
40 |
41 | this.trail.forEach((point, i) => {
42 | this.drawTouch(point);
43 | });
44 |
45 | this.texture.needsUpdate = true;
46 | }
47 |
48 | clear() {
49 | this.ctx.fillStyle = 'black';
50 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
51 | }
52 |
53 | addTouch(point) {
54 | let force = 0;
55 | const last = this.trail[this.trail.length - 1];
56 | if (last) {
57 | const dx = last.x - point.x;
58 | const dy = last.y - point.y;
59 | const dd = dx * dx + dy * dy;
60 | force = Math.min(dd * 10000, 1);
61 | }
62 | this.trail.push({ x: point.x, y: point.y, age: 0, force });
63 | }
64 |
65 | drawTouch(point) {
66 | const pos = {
67 | x: point.x * this.size,
68 | y: (1 - point.y) * this.size
69 | };
70 |
71 | let intensity = 1;
72 | if (point.age < this.maxAge * 0.3) {
73 | intensity = easeOutSine(point.age / (this.maxAge * 0.3), 0, 1, 1);
74 | } else {
75 | intensity = easeOutSine(1 - (point.age - this.maxAge * 0.3) / (this.maxAge * 0.7), 0, 1, 1);
76 | }
77 |
78 | intensity *= point.force;
79 |
80 | const radius = this.size * this.radius * intensity;
81 | const grd = this.ctx.createRadialGradient(pos.x, pos.y, radius * 0.25, pos.x, pos.y, radius);
82 | grd.addColorStop(0, `rgba(255, 255, 255, 0.2)`);
83 | grd.addColorStop(1, 'rgba(0, 0, 0, 0.0)');
84 |
85 | this.ctx.beginPath();
86 | this.ctx.fillStyle = grd;
87 | this.ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
88 | this.ctx.fill();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/shaders/particle.frag:
--------------------------------------------------------------------------------
1 | // @author brunoimbrizi / http://brunoimbrizi.com
2 |
3 | precision highp float;
4 |
5 | uniform sampler2D uTexture;
6 |
7 | varying vec2 vPUv;
8 | varying vec2 vUv;
9 |
10 | void main() {
11 | vec4 color = vec4(0.0);
12 | vec2 uv = vUv;
13 | vec2 puv = vPUv;
14 |
15 | // pixel color
16 | vec4 colA = texture2D(uTexture, puv);
17 |
18 | // greyscale
19 | //float grey = colA.r * 0.21 + colA.g * 0.71 + colA.b * 0.07;
20 | //vec4 colB = vec4(grey, grey, grey, 1.0);
21 |
22 |
23 | vec4 colB = vec4(colA.r, colA.g, colA.b, 1.0);
24 |
25 | // circle
26 | float border = 0.3;
27 | float radius = 0.5;
28 | float dist = radius - distance(uv, vec2(0.5));
29 | float t = smoothstep(0.0, border, dist);
30 |
31 | // final color
32 | color = colB;
33 | color.a = t;
34 |
35 | gl_FragColor = color;
36 | }
--------------------------------------------------------------------------------
/src/shaders/particle.vert:
--------------------------------------------------------------------------------
1 | // @author brunoimbrizi / http://brunoimbrizi.com
2 |
3 | precision highp float;
4 |
5 | attribute float pindex;
6 | attribute vec3 position;
7 | attribute vec3 offset;
8 | attribute vec2 uv;
9 | attribute float angle;
10 |
11 | uniform mat4 modelViewMatrix;
12 | uniform mat4 projectionMatrix;
13 |
14 | uniform float uTime;
15 | uniform float uRandom;
16 | uniform float uDepth;
17 | uniform float uSize;
18 | uniform vec2 uTextureSize;
19 | uniform sampler2D uTexture;
20 | uniform sampler2D uTouch;
21 |
22 | varying vec2 vPUv;
23 | varying vec2 vUv;
24 |
25 | #pragma glslify: snoise2 = require(glsl-noise/simplex/2d)
26 |
27 | float random(float n) {
28 | return fract(sin(n) * 43758.5453123);
29 | }
30 |
31 | void main() {
32 | vUv = uv;
33 |
34 | // particle uv
35 | vec2 puv = offset.xy / uTextureSize;
36 | vPUv = puv;
37 |
38 | // pixel color
39 | vec4 colA = texture2D(uTexture, puv);
40 | float grey = colA.r * 0.21 + colA.g * 0.71 + colA.b * 0.07;
41 |
42 | // displacement
43 | vec3 displaced = offset;
44 | // randomise
45 | displaced.xy += vec2(random(pindex) - 0.5, random(offset.x + pindex) - 0.5) * uRandom;
46 | float rndz = (random(pindex) + snoise_1_2(vec2(pindex * 0.1, uTime * 0.1)));
47 | displaced.z += rndz * (random(pindex) * 2.0 * uDepth);
48 | // center
49 | displaced.xy -= uTextureSize * 0.5;
50 |
51 | // touch
52 | float t = texture2D(uTouch, puv).r;
53 | displaced.z += t * 20.0 * rndz;
54 | displaced.x += cos(angle) * t * 20.0 * rndz;
55 | displaced.y += sin(angle) * t * 20.0 * rndz;
56 |
57 | // particle size
58 | float psize = (snoise_1_2(vec2(uTime, pindex) * 0.5) + 2.0);
59 | psize *= max(grey, 0.2);
60 | psize *= uSize;
61 |
62 | // final position
63 | vec4 mvPosition = modelViewMatrix * vec4(displaced, 1.0);
64 | mvPosition.xyz += position * psize;
65 | vec4 finalPosition = projectionMatrix * mvPosition;
66 |
67 | gl_Position = finalPosition;
68 | }
69 |
--------------------------------------------------------------------------------
/static/css/base.css:
--------------------------------------------------------------------------------
1 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;}
2 | *,
3 | *::after,
4 | *::before {
5 | box-sizing: border-box;
6 | }
7 |
8 | :root {
9 | font-size: 16px;
10 | }
11 |
12 | body {
13 | --color-text: #fff;
14 | --color-bg: #0e0e0f;
15 | --color-link: #EE9A00;
16 | --color-link-hover: #8e5c00;
17 | color: var(--color-text);
18 | background-color: var(--color-bg);
19 | font-family: Futura, "futura-pt", Arial, sans-serif;
20 | -webkit-font-smoothing: antialiased;
21 | -moz-osx-font-smoothing: grayscale;
22 | }
23 |
24 | a {
25 | text-decoration: none;
26 | color: var(--color-link);
27 | outline: none;
28 | }
29 |
30 | a:hover,
31 | a:focus {
32 | color: var(--color-link-hover);
33 | outline: none;
34 | }
35 |
36 | .frame {
37 | padding: 3rem 5vw;
38 | text-align: center;
39 | position: relative;
40 | z-index: 1000;
41 | }
42 |
43 | .frame__title {
44 | font-size: 1rem;
45 | margin: 0 0 1rem;
46 | font-weight: normal;
47 | }
48 |
49 | .frame__links {
50 | display: inline;
51 | }
52 |
53 | .frame a {
54 | text-transform: lowercase;
55 | }
56 |
57 | .frame__github,
58 | .frame__links a:not(:last-child),
59 | .frame__demos a:not(:last-child) {
60 | margin-right: 1rem;
61 | }
62 |
63 | .content {
64 | display: flex;
65 | flex-direction: column;
66 | width: 100vw;
67 | height: calc(100vh - 13rem);
68 | position: relative;
69 | justify-content: flex-start;
70 | align-items: center;
71 | }
72 | .panel{
73 | display: none;
74 | }
75 | @media screen and (min-width: 53em) {
76 | .frame {
77 | position: fixed;
78 | text-align: left;
79 | z-index: 10000;
80 | top: 0;
81 | left: 0;
82 | display: grid;
83 | align-content: space-between;
84 | width: 100%;
85 | max-width: none;
86 | height: 100vh;
87 | padding: 3rem;
88 | pointer-events: none;
89 | grid-template-columns: 75% 25%;
90 | grid-template-rows: auto auto auto;
91 | grid-template-areas: 'title links'
92 | '... ...'
93 | 'github demos';
94 | }
95 | .frame__title-wrap {
96 | grid-area: title;
97 | display: flex;
98 | }
99 | .frame__title {
100 | margin: 0;
101 | }
102 | .frame__tagline {
103 | position: relative;
104 | margin: 0 0 0 1rem;
105 | padding: 0 0 0 1rem;
106 | opacity: 0.5;
107 | }
108 | .frame__github {
109 | grid-area: github;
110 | justify-self: start;
111 | margin: 0;
112 | }
113 | .frame__links {
114 | grid-area: links;
115 | padding: 0;
116 | justify-self: end;
117 | }
118 | .frame a {
119 | pointer-events: auto;
120 | }
121 | .content {
122 | height: 100vh;
123 | justify-content: center;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/static/css/demo1.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #222;
3 | margin: 0;
4 | overflow: hidden;
5 | cursor: pointer;
6 | }
7 | canvas{
8 | width: 100%;
9 | height: auto;
10 | }
--------------------------------------------------------------------------------
/static/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/1.png
--------------------------------------------------------------------------------
/static/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/2.png
--------------------------------------------------------------------------------
/static/images/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/avatar.jpg
--------------------------------------------------------------------------------
/static/images/sample-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/sample-01.png
--------------------------------------------------------------------------------
/static/images/sample-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/sample-02.png
--------------------------------------------------------------------------------
/static/images/sample-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/sample-03.png
--------------------------------------------------------------------------------
/static/images/sample-04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/sample-04.png
--------------------------------------------------------------------------------
/static/images/skills/D3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/D3.png
--------------------------------------------------------------------------------
/static/images/skills/antd.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/antd.webp
--------------------------------------------------------------------------------
/static/images/skills/aws.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/aws.png
--------------------------------------------------------------------------------
/static/images/skills/azure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/azure.png
--------------------------------------------------------------------------------
/static/images/skills/babylon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/babylon.png
--------------------------------------------------------------------------------
/static/images/skills/binanceCoin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/binanceCoin.jpg
--------------------------------------------------------------------------------
/static/images/skills/bitcoin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/bitcoin.png
--------------------------------------------------------------------------------
/static/images/skills/blockchain.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/blockchain.jpg
--------------------------------------------------------------------------------
/static/images/skills/bootstrap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/bootstrap.png
--------------------------------------------------------------------------------
/static/images/skills/chartjs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
53 |
--------------------------------------------------------------------------------
/static/images/skills/ci.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/skills/cloudmongo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/cloudmongo.png
--------------------------------------------------------------------------------
/static/images/skills/csharp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/skills/css3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/skills/django.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/django.png
--------------------------------------------------------------------------------
/static/images/skills/docker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/docker.png
--------------------------------------------------------------------------------
/static/images/skills/ethereum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/ethereum.png
--------------------------------------------------------------------------------
/static/images/skills/express.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/express.png
--------------------------------------------------------------------------------
/static/images/skills/firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/firebase.png
--------------------------------------------------------------------------------
/static/images/skills/flask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/flask.png
--------------------------------------------------------------------------------
/static/images/skills/git.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/git.png
--------------------------------------------------------------------------------
/static/images/skills/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/github.png
--------------------------------------------------------------------------------
/static/images/skills/gitlab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/gitlab.png
--------------------------------------------------------------------------------
/static/images/skills/googlecloud.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/skills/graphql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/graphql.png
--------------------------------------------------------------------------------
/static/images/skills/graphql.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/skills/html.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/html.png
--------------------------------------------------------------------------------
/static/images/skills/jasmine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/jasmine.png
--------------------------------------------------------------------------------
/static/images/skills/jquery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/jquery.png
--------------------------------------------------------------------------------
/static/images/skills/js.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/js.jpg
--------------------------------------------------------------------------------
/static/images/skills/laravel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/laravel.jpg
--------------------------------------------------------------------------------
/static/images/skills/linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/linux.png
--------------------------------------------------------------------------------
/static/images/skills/litecoin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/litecoin.jpg
--------------------------------------------------------------------------------
/static/images/skills/macox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/macox.png
--------------------------------------------------------------------------------
/static/images/skills/materialUI.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/materialUI.png
--------------------------------------------------------------------------------
/static/images/skills/mocha.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/images/skills/mongodb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/mongodb.jpg
--------------------------------------------------------------------------------
/static/images/skills/mysql.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/mysql.webp
--------------------------------------------------------------------------------
/static/images/skills/next.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/next.jpg
--------------------------------------------------------------------------------
/static/images/skills/node.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/node.jpg
--------------------------------------------------------------------------------
/static/images/skills/php.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/php.png
--------------------------------------------------------------------------------
/static/images/skills/postgresql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/postgresql.png
--------------------------------------------------------------------------------
/static/images/skills/postman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/postman.png
--------------------------------------------------------------------------------
/static/images/skills/python.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/python.png
--------------------------------------------------------------------------------
/static/images/skills/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/react.png
--------------------------------------------------------------------------------
/static/images/skills/reactNative.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/reactNative.png
--------------------------------------------------------------------------------
/static/images/skills/redux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/redux.png
--------------------------------------------------------------------------------
/static/images/skills/sass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/sass.png
--------------------------------------------------------------------------------
/static/images/skills/selenium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/selenium.png
--------------------------------------------------------------------------------
/static/images/skills/sqlite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/sqlite.png
--------------------------------------------------------------------------------
/static/images/skills/tailwind.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/tailwind.png
--------------------------------------------------------------------------------
/static/images/skills/three.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/three.png
--------------------------------------------------------------------------------
/static/images/skills/ts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/ts.png
--------------------------------------------------------------------------------
/static/images/skills/vue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/vue.png
--------------------------------------------------------------------------------
/static/images/skills/webRTC.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/webRTC.png
--------------------------------------------------------------------------------
/static/images/skills/webgl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/webgl.png
--------------------------------------------------------------------------------
/static/images/skills/windows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamond-softdev/threejs-interactive-particles-image/e48e542905f30aeffb6fdff441337caafa0bcd62/static/images/skills/windows.png
--------------------------------------------------------------------------------