A particle emitter using DOM elements and CSS transforms instead of more sensible technologies like SVG or Canvas.
A Pen by Ashley Kyd on CodePen.
A particle emitter using DOM elements and CSS transforms instead of more sensible technologies like SVG or Canvas.
A Pen by Ashley Kyd on CodePen.
| <div id="box"> | |
| <h1>DOM particle emitter</h1> | |
| <p>So this is kinda neat I guess, it's a particle emitter using DOM elements instead of more sensible technologies like SVG or Canvas.</p> | |
| <p>For bonus points/performance, this has been updated to use CSS transitions rather than requestAnimationFrame.</p> | |
| <p>Click for an explosion.</p> | |
| </div> |
| var emitters = []; | |
| // http://davidwalsh.name/vendor-prefix | |
| var prefix = (function () { | |
| var styles = window.getComputedStyle(document.documentElement, ''), | |
| pre = (Array.prototype.slice | |
| .call(styles) | |
| .join('') | |
| .match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o']) | |
| )[1], | |
| dom = ('WebKit|Moz|MS|O').match(new RegExp('(' + pre + ')', 'i'))[1]; | |
| return { | |
| dom: dom, | |
| lowercase: pre, | |
| css: '-' + pre + '-', | |
| js: pre[0].toUpperCase() + pre.substr(1) | |
| }; | |
| })(); | |
| var Emitter = function(opts){ | |
| this.particles = []; | |
| this.opts = opts; | |
| this.total = 0; | |
| this.opts.i = Date.now(); | |
| this.particleType = opts.particleType || Particle; | |
| this.ele = document.createElement('div'); | |
| this.ele.className = 'emitter'; | |
| this.ele.setAttribute('aria-hidden','true'); | |
| this.ele.setAttribute('style', 'position:absolute;transform:translate('+this.opts.center[0]+'px, '+this.opts.center[1]+'px);'); | |
| for(var i=0; i<opts.particles;i++){ | |
| this.total++; | |
| this.particles.push(new this.particleType(this.ele, opts)); | |
| } | |
| document.body.appendChild(this.ele); | |
| } | |
| Emitter.prototype.destroy = function(){ | |
| this.ele.parentNode.removeChild(this.ele); | |
| } | |
| var Particle = function(parent, opts){ | |
| // this.ele = this.pool.pop(); // Broken | |
| if(!this.ele){ | |
| this.ele = document.createElement('div'); | |
| this.ele.setAttribute('style','position:absolute;'); | |
| this.ele.className = 'particle'; | |
| } | |
| parent.appendChild(this.ele); | |
| this.reset(opts); | |
| var _this = this; | |
| this.ele.addEventListener( prefix.lowercase+'TransitionEnd', | |
| function( event ) { | |
| if(opts.loop){ | |
| _this.reset(opts, 0); | |
| } else { | |
| console.log('cleaning up') | |
| if(_this.ele.parentElement){ | |
| _this.ele.parentElement.removeChild(_this.ele); | |
| } | |
| _this.pool.push(_this.ele); | |
| } | |
| }, false ); | |
| } | |
| // Save old particles here because creating them is OMG EXPENSIVE. | |
| Particle.prototype.pool = []; | |
| Particle.prototype.beforeDraw = function(p){ | |
| this.ele.style.opacity = p; | |
| } | |
| Particle.prototype.reset = function(opts, p){ | |
| if(opts.colorFn){ | |
| this.color = opts.colorFn(); | |
| } else { | |
| this.color = opts.color || 'white'; | |
| } | |
| this.r = this.fuzz(opts.r) || 4; | |
| this.ang = this.fuzz(opts.ang) || Math.PI*2*Math.random(); | |
| this.spd = this.fuzz(opts.spd) || Math.random()/5; | |
| this.life = this.fuzz(opts.life) || 250+Math.random() * 250; | |
| this.i = opts.i || 0; | |
| this.animate = opts.animate || ['scale']; | |
| this.rNow = this.r; | |
| this.angNow = Math.PI/2+ this.ang; | |
| this.spdNow = this.spd; | |
| this.colorNow = this.color; | |
| this.draw(0,1); | |
| var _this = this; | |
| window.setTimeout(function(){ | |
| _this.draw(_this.life,0); | |
| }); | |
| } | |
| Particle.prototype.fuzz = function(value){ | |
| if(!value){ | |
| return false; | |
| } | |
| return value[0] + (value[1]*2*Math.random()-value[1]); | |
| } | |
| Particle.prototype.draw = function(i,p){ | |
| if(p===1){ | |
| this.ele.classList.add('notransition'); | |
| } else { | |
| this.ele.style[prefix.lowercase+'TransitionDuration'] = this.life/1000+'s'; | |
| this.ele.classList.remove('notransition'); | |
| } | |
| this.beforeDraw(p); | |
| var radiusOffset = (0-this.rNow/2); | |
| this.ele.style.transform = [ | |
| 'translate('+radiusOffset+'px,'+radiusOffset+'px)', // Center on circle | |
| 'rotate('+this.angNow+'rad)', // Rotate in whichever direction we're emitting. | |
| 'translate('+(i*this.spdNow)+'px,0)' // Move however far we need to go. | |
| ].join(' '); | |
| this.ele.style.width = this.rNow+'px'; | |
| this.ele.style.height = this.rNow+'px'; | |
| this.ele.style.backgroundColor = this.colorNow; | |
| this.ele.style.borderRadius = this.rNow+'px'; | |
| } | |
| var ParticleShrink = function(parent, opts){ | |
| Particle.call(this, parent, opts); | |
| } | |
| for(var i in Particle.prototype){ | |
| ParticleShrink.prototype[i] = Particle.prototype[i]; | |
| } | |
| ParticleShrink.prototype.beforeDraw = function(p){ | |
| this.rNow = this.r * p; | |
| } | |
| function explode(x,y){ | |
| emitters.push(new Emitter({ | |
| particles:5, | |
| particlesTotal:10, | |
| colorFn:function(){ | |
| var color = 'rgba(128,128,128,'+Math.random()*.5+')'; | |
| return color;; | |
| }, | |
| ang: [Math.PI,Math.PI], | |
| r: [25,10], | |
| spd: [.03,.02], | |
| life: [1000,800], | |
| center: [x,y] | |
| })); | |
| emitters.push(new Emitter({ | |
| particles:20, | |
| particlesTotal:10, | |
| colorFn:function(){ | |
| var color = 'rgba(255,0,0,'+Math.random()*.5+')'; | |
| return color;; | |
| }, | |
| ang: [Math.PI,Math.PI], | |
| r: [16,6], | |
| spd: [.1,.05], | |
| life: [800,250], | |
| center: [x,y] | |
| })); | |
| emitters.push(new Emitter({ | |
| particles:15, | |
| particlesTotal:15, | |
| colorFn:function(){ | |
| var color = 'rgba(255,255,0,1)'; | |
| return color;; | |
| }, | |
| ang: [Math.PI,Math.PI], | |
| r: [5,3], | |
| spd: [.2,.05], | |
| life: [600,250], | |
| center: [x,y] | |
| })); | |
| } | |
| function flames(x,y){ | |
| emitters.push(new Emitter({ | |
| particles:40, | |
| particlesTotal:-1, | |
| color:'rgba(90,90,90,.5)', | |
| ang: [Math.PI,.5], | |
| r: [10,8], | |
| spd: [.01,.005], | |
| life: [5000,5000], | |
| center: [x,y], | |
| loop:true | |
| })); | |
| /* Engine Flames */ | |
| emitters.push(new Emitter({ | |
| particles:40, | |
| particlesTotal:-1, | |
| color:'red', | |
| ang: [Math.PI,.5], | |
| r: [8,3], | |
| spd: [.02,.0025], | |
| life: [2000,2000], | |
| center: [x,y], | |
| loop:true | |
| })); | |
| emitters.push(new Emitter({ | |
| particles:60, | |
| particleType: ParticleShrink, | |
| particlesTotal:-1, | |
| color:'#f40', | |
| ang: [Math.PI,.3], | |
| r: [5,2], | |
| spd: [.04,.01], | |
| life: [3000,1000], | |
| center: [x,y], | |
| loop:true | |
| })); | |
| emitters.push(new Emitter({ | |
| particles:40, | |
| particleType: ParticleShrink, | |
| particlesTotal:-1, | |
| color:'orange', | |
| ang: [Math.PI,.25], | |
| r: [4,2], | |
| spd: [.02,.01], | |
| life: [3000,300], | |
| center: [x,y], | |
| loop:true | |
| })); | |
| emitters.push(new Emitter({ | |
| particles:40, | |
| particlesTotal:-1, | |
| color:'rgba(255,255,255,.5)', | |
| ang: [Math.PI,.25], | |
| r: [3,2], | |
| spd: [.02,.01], | |
| life: [2000,300], | |
| center: [x,y], | |
| loop:true | |
| })); | |
| } | |
| flames(200,300); | |
| document.body.onclick = function(e){ | |
| explode(e.clientX,e.clientY); | |
| } | |
| document.body.style.height=window.innerHeight+'px'; |
| body{ | |
| background:black; | |
| color:white; | |
| margin:0; | |
| padding:0; | |
| font-family:sans-serif; | |
| } | |
| #box{ | |
| position:absolute; | |
| max-width:400px; | |
| padding:20px; | |
| } | |
| .particle{ | |
| transition: all 1s; | |
| } | |
| .notransition { | |
| transition: none !important; | |
| } |