├── .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 | ![cover](cover.png) 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 | 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 | 5 | 6 | 7 | 12 | 16 | 23 | 26 | 30 | 32 | 36 | 42 | 43 | 44 | 46 | 48 | 50 | 52 | 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 | Main -------------------------------------------------------------------------------- /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 | Mocha Logoimage/svg+xmlMocha Logo08-21-2015Dick DeLeon <ddeleon@decipherinc.com>CC BY-SA 4.0mochamochajsChristopher Hiller <boneskull@boneskull.com> -------------------------------------------------------------------------------- /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 --------------------------------------------------------------------------------