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; | |
} |