/* * @author zz85 (http://github.com/zz85 http://www.lab4games.net/zz85/blog) * * a simple to use javascript 3d particles system inspired by FliNT and Stardust * created with TWEEN.js and THREE.js * * for feature requests or bugs, please visit https://github.com/zz85/sparks.js * * licensed under the MIT license */ var SPARKS = {}; /******************************** * Emitter Class * * Creates and Manages Particles *********************************/ SPARKS.Engine = { // Combined Singleton Engine; _TIMESTEP: 15, _timer: null, _lastTime: null, _timerStep: 10, _velocityVerlet: false, _emitters: [], _isRunning: false, add: function(emitter) { this._emitters.push(emitter); }, // run its built in timer / stepping start: function() { this._lastTime = Date.now(); this._timer = setTimeout(this.step, this._timerStep, this); for (var i=0,il=this._emitters.length;i= maxBlock) { //console.log('warning: sparks.js is fast fowarding engine, skipping steps', elapsed / emitter._TIMESTEP); //emitter.update( (elapsed - maxBlock) / 1000); elapsed = maxBlock; } while(elapsed >= me._TIMESTEP) { me.update(me._TIMESTEP / 1000); elapsed -= me._TIMESTEP; } me._lastTime = time - elapsed; } else { me.update(elapsed/1000); me._lastTime = time; } setTimeout(me.step, me._timerStep, me); }, update: function(time) { for (var i=0,il=this._emitters.length;i= maxBlock) { //console.log('warning: sparks.js is fast fowarding engine, skipping steps', elapsed / emitter._TIMESTEP); //emitter.update( (elapsed - maxBlock) / 1000); elapsed = maxBlock; } while(elapsed >= emitter._TIMESTEP) { emitter.update(emitter._TIMESTEP / 1000); elapsed -= emitter._TIMESTEP; } emitter._lastTime = time - elapsed; } else { emitter.update(elapsed/1000); emitter._lastTime = time; } if (emitter._isRunning) setTimeout(emitter.step, emitter._timerStep, emitter); }, // Update particle engine in seconds, not milliseconds update: function(time) { var len = this._counter.updateEmitter( this, time ); // Create particles for( i = 0; i < len; i++ ) { this.createParticle(); } // Update activities len = this._activities.length; for ( i = 0; i < len; i++ ) { this._activities[i].update( this, time ); } len = this._actions.length; var action; var len2 = this._particles.length; for( j = 0; j < len; j++ ) { action = this._actions[j]; for ( i = 0; i < len2; ++i ) { particle = this._particles[i]; action.update( this, particle, time ); } } // remove dead particles for ( i = len2; i--; ) { particle = this._particles[i]; if ( particle.isDead ) { //particle = this._particles.splice( i, 1 ); this.dispatchEvent("dead", particle); SPARKS.VectorPool.release(particle.position); // SPARKS.VectorPool.release(particle.velocity); } else { this.dispatchEvent("updated", particle); } } this.dispatchEvent("loopUpdated"); }, createParticle: function() { var particle = new SPARKS.Particle(); // In future, use a Particle Factory var len = this._initializers.length, i; for ( i = 0; i < len; i++ ) { this._initializers[i].initialize( this, particle ); } this._particles.push( particle ); this.dispatchEvent("created", particle); // ParticleCreated return particle; }, addInitializer: function (initializer) { this._initializers.push(initializer); }, addAction: function (action) { this._actions.push(action); }, removeInitializer: function (initializer) { var index = this._initializers.indexOf(initializer); if (index > -1) { this._initializers.splice( index, 1 ); } }, removeAction: function (action) { var index = this._actions.indexOf(action); if (index > -1) { this._actions.splice( index, 1 ); } //console.log('removeAction', index, this._actions); }, addCallback: function(name, callback) { this.callbacks[name] = callback; }, removeCallback: function(name) { delete this.callbacks[name]; }, dispatchEvent: function(name, args) { var callback = this.callbacks[name]; if (callback) { callback(args); } } }; /* * Constant Names for * Events called by emitter.dispatchEvent() * */ SPARKS.EVENT_PARTICLE_CREATED = "created" SPARKS.EVENT_PARTICLE_UPDATED = "updated" SPARKS.EVENT_PARTICLE_DEAD = "dead"; SPARKS.EVENT_LOOP_UPDATED = "loopUpdated"; /* * Steady Counter attempts to produces a particle rate steadily * */ // Number of particles per seconds SPARKS.SteadyCounter = function(rate) { this.rate = rate; // we use a shortfall counter to make up for slow emitters this.leftover = 0; }; SPARKS.SteadyCounter.prototype.updateEmitter = function(emitter, time) { var targetRelease = time * this.rate + this.leftover; var actualRelease = Math.floor(targetRelease); this.leftover = targetRelease - actualRelease; return actualRelease; }; /* * Shot Counter produces specified particles * on a single impluse or burst */ SPARKS.ShotCounter = function(particles) { this.particles = particles; this.used = false; }; SPARKS.ShotCounter.prototype.updateEmitter = function(emitter, time) { if (this.used) { return 0; } else { this.used = true; } return this.particles; }; /******************************** * Particle Class * * Represents a single particle *********************************/ SPARKS.Particle = function() { /** * The lifetime of the particle, in seconds. */ this.lifetime = 0; /** * The age of the particle, in seconds. */ this.age = 0; /** * The energy of the particle. */ this.energy = 1; /** * Whether the particle is dead and should be removed from the stage. */ this.isDead = false; this.target = null; // tag /** * For 3D */ this.position = SPARKS.VectorPool.get().set(0,0,0); //new THREE.Vector3( 0, 0, 0 ); this.velocity = SPARKS.VectorPool.get().set(0,0,0); //new THREE.Vector3( 0, 0, 0 ); this._oldvelocity = SPARKS.VectorPool.get().set(0,0,0); // rotation vec3 // angVelocity vec3 // faceAxis vec3 }; /******************************** * Action Classes * * An abstract class which have * update function *********************************/ SPARKS.Action = function() { this._priority = 0; }; SPARKS.Age = function(easing) { this._easing = (easing == null) ? TWEEN.Easing.Linear.EaseNone : easing; }; SPARKS.Age.prototype.update = function (emitter, particle, time) { particle.age += time; if( particle.age >= particle.lifetime ) { particle.energy = 0; particle.isDead = true; } else { var t = this._easing(particle.age / particle.lifetime); particle.energy = -1 * t + 1; } }; /* // Mark particle as dead when particle's < 0 SPARKS.Death = function(easing) { this._easing = (easing == null) ? TWEEN.Linear.EaseNone : easing; }; SPARKS.Death.prototype.update = function (emitter, particle, time) { if (particle.life <= 0) { particle.isDead = true; } }; */ SPARKS.Move = function() { }; SPARKS.Move.prototype.update = function(emitter, particle, time) { // attempt verlet velocity updating. var p = particle.position; var v = particle.velocity; var old = particle._oldvelocity; if (this._velocityVerlet) { p.x += (v.x + old.x) * 0.5 * time; p.y += (v.y + old.y) * 0.5 * time; p.z += (v.z + old.z) * 0.5 * time; } else { p.x += v.x * time; p.y += v.y * time; p.z += v.z * time; } // OldVel = Vel; // Vel = Vel + Accel * dt; // Pos = Pos + (vel + Vel + Accel * dt) * 0.5 * dt; }; /* Marks particles found in specified zone dead */ SPARKS.DeathZone = function(zone) { this.zone = zone; }; SPARKS.DeathZone.prototype.update = function(emitter, particle, time) { if (this.zone.contains(particle.position)) { particle.isDead = true; } }; /* * SPARKS.ActionZone applies an action when particle is found in zone */ SPARKS.ActionZone = function(action, zone) { this.action = action; this.zone = zone; }; SPARKS.ActionZone.prototype.update = function(emitter, particle, time) { if (this.zone.contains(particle.position)) { this.action.update( emitter, particle, time ); } }; /* * Accelerate action affects velocity in specified 3d direction */ SPARKS.Accelerate = function(x,y,z) { if (x instanceof THREE.Vector3) { this.acceleration = x; return; } this.acceleration = new THREE.Vector3(x,y,z); }; SPARKS.Accelerate.prototype.update = function(emitter, particle, time) { var acc = this.acceleration; var v = particle.velocity; particle._oldvelocity.set(v.x, v.y, v.z); v.x += acc.x * time; v.y += acc.y * time; v.z += acc.z * time; }; /* * Accelerate Factor accelerate based on a factor of particle's velocity. */ SPARKS.AccelerateFactor = function(factor) { this.factor = factor; }; SPARKS.AccelerateFactor.prototype.update = function(emitter, particle, time) { var factor = this.factor; var v = particle.velocity; var len = v.length(); var adjFactor; if (len>0) { adjFactor = factor * time / len; adjFactor += 1; v.multiplyScalar(adjFactor); } }; /* AccelerateNormal * AccelerateVelocity affects velocity based on its velocity direction */ SPARKS.AccelerateVelocity = function(factor) { this.factor = factor; }; SPARKS.AccelerateVelocity.prototype.update = function(emitter, particle, time) { var factor = this.factor; var v = particle.velocity; v.z += - v.x * factor; v.y += v.z * factor; v.x += v.y * factor; }; /* Set the max ammount of x,y,z drift movements in a second */ SPARKS.RandomDrift = function(x,y,z) { if (x instanceof THREE.Vector3) { this.drift = x; return; } this.drift = new THREE.Vector3(x,y,z); } SPARKS.RandomDrift.prototype.update = function(emitter, particle, time) { var drift = this.drift; var v = particle.velocity; v.x += ( Math.random() - 0.5 ) * drift.x * time; v.y += ( Math.random() - 0.5 ) * drift.y * time; v.z += ( Math.random() - 0.5 ) * drift.z * time; }; /******************************** * Zone Classes * * An abstract classes which have * getLocation() function *********************************/ SPARKS.Zone = function() { }; // TODO, contains() for Zone SPARKS.PointZone = function(pos) { this.pos = pos; }; SPARKS.PointZone.prototype.getLocation = function() { return this.pos; }; SPARKS.PointZone = function(pos) { this.pos = pos; }; SPARKS.PointZone.prototype.getLocation = function() { return this.pos; }; SPARKS.LineZone = function(start, end) { this.start = start; this.end = end; this._length = end.clone().sub( start ); }; SPARKS.LineZone.prototype.getLocation = function() { var len = this._length.clone(); len.multiplyScalar( Math.random() ); return len.add( this.start ); }; // Basically a RectangleZone SPARKS.ParallelogramZone = function(corner, side1, side2) { this.corner = corner; this.side1 = side1; this.side2 = side2; }; SPARKS.ParallelogramZone.prototype.getLocation = function() { var d1 = this.side1.clone().multiplyScalar( Math.random() ); var d2 = this.side2.clone().multiplyScalar( Math.random() ); d1.add(d2); return d1.add( this.corner ); }; SPARKS.CubeZone = function(position, x, y, z) { this.position = position; this.x = x; this.y = y; this.z = z; }; SPARKS.CubeZone.prototype.getLocation = function() { //TODO use pool? var location = this.position.clone(); location.x += Math.random() * this.x; location.y += Math.random() * this.y; location.z += Math.random() * this.z; return location; }; SPARKS.CubeZone.prototype.contains = function(position) { var startX = this.position.x; var startY = this.position.y; var startZ = this.position.z; var x = this.x; // width var y = this.y; // depth var z = this.z; // height if (x<0) { startX += x; x = Math.abs(x); } if (y<0) { startY += y; y = Math.abs(y); } if (z<0) { startZ += z; z = Math.abs(z); } var diffX = position.x - startX; var diffY = position.y - startY; var diffZ = position.z - startZ; if ( (diffX > 0) && (diffX < x) && (diffY > 0) && (diffY < y) && (diffZ > 0) && (diffZ < z) ) { return true; } return false; }; /** * The constructor creates a DiscZone 3D zone. * * @param centre The point at the center of the disc. * @param normal A vector normal to the disc. * @param outerRadius The outer radius of the disc. * @param innerRadius The inner radius of the disc. This defines the hole * in the center of the disc. If set to zero, there is no hole. */ /* // BUGGY!! SPARKS.DiscZone = function(center, radiusNormal, outerRadius, innerRadius) { this.center = center; this.radiusNormal = radiusNormal; this.outerRadius = (outerRadius==undefined) ? 0 : outerRadius; this.innerRadius = (innerRadius==undefined) ? 0 : innerRadius; }; SPARKS.DiscZone.prototype.getLocation = function() { var rand = Math.random(); var _innerRadius = this.innerRadius; var _outerRadius = this.outerRadius; var center = this.center; var _normal = this.radiusNormal; _distToOrigin = _normal.dot( center ); var radius = _innerRadius + (1 - rand * rand ) * ( _outerRadius - _innerRadius ); var angle = Math.random() * SPARKS.Utils.TWOPI; var _distToOrigin = _normal.dot( center ); var axes = SPARKS.Utils.getPerpendiculars( _normal.clone() ); var _planeAxis1 = axes[0]; var _planeAxis2 = axes[1]; var p = _planeAxis1.clone(); p.multiplyScalar( radius * Math.cos( angle ) ); var p2 = _planeAxis2.clone(); p2.multiplyScalar( radius * Math.sin( angle ) ); p.add( p2 ); return _center.add( p ); }; */ SPARKS.SphereCapZone = function(x, y, z, minr, maxr, angle) { this.x = x; this.y = y; this.z = z; this.minr = minr; this.maxr = maxr; this.angle = angle; }; SPARKS.SphereCapZone.prototype.getLocation = function() { var theta = Math.PI *2 * SPARKS.Utils.random(); var r = SPARKS.Utils.random(); //new THREE.Vector3 var v = SPARKS.VectorPool.get().set(r * Math.cos(theta), -1 / Math.tan(this.angle * SPARKS.Utils.DEGREE_TO_RADIAN), r * Math.sin(theta)); //v.length = StardustMath.interpolate(0, _minRadius, 1, _maxRadius, Math.random()); var i = this.minr - ((this.minr-this.maxr) * Math.random() ); v.multiplyScalar(i); v.__markedForReleased = true; return v; }; /******************************** * Initializer Classes * * Classes which initializes * particles. Implements initialize( emitter:Emitter, particle:Particle ) *********************************/ // Specifies random life between max and min SPARKS.Lifetime = function(min, max) { this._min = min; this._max = max ? max : min; }; SPARKS.Lifetime.prototype.initialize = function( emitter/*Emitter*/, particle/*Particle*/ ) { particle.lifetime = this._min + SPARKS.Utils.random() * ( this._max - this._min ); }; SPARKS.Position = function(zone) { this.zone = zone; }; SPARKS.Position.prototype.initialize = function( emitter/*Emitter*/, particle/*Particle*/ ) { var pos = this.zone.getLocation(); particle.position.set(pos.x, pos.y, pos.z); }; SPARKS.Velocity = function(zone) { this.zone = zone; }; SPARKS.Velocity.prototype.initialize = function( emitter/*Emitter*/, particle/*Particle*/ ) { var pos = this.zone.getLocation(); particle.velocity.set(pos.x, pos.y, pos.z); if (pos.__markedForReleased) { //console.log("release"); SPARKS.VectorPool.release(pos); pos.__markedForReleased = false; } }; SPARKS.Target = function(target, callback) { this.target = target; this.callback = callback; }; SPARKS.Target.prototype.initialize = function( emitter, particle ) { if (this.callback) { particle.target = this.callback(); } else { particle.target = this.target; } }; /******************************** * VectorPool * * Reuse much of Vectors if possible *********************************/ SPARKS.VectorPool = { __pools: [], // Get a new Vector get: function() { if (this.__pools.length>0) { return this.__pools.pop(); } return this._addToPool(); }, // Release a vector back into the pool release: function(v) { this.__pools.push(v); }, // Create a bunch of vectors and add to the pool _addToPool: function() { //console.log("creating some pools"); for (var i=0, size = 100; i < size; i++) { this.__pools.push(new THREE.Vector3()); } return new THREE.Vector3(); } }; /******************************** * Util Classes * * Classes which initializes * particles. Implements initialize( emitter:Emitter, particle:Particle ) *********************************/ SPARKS.Utils = { random: function() { return Math.random(); }, DEGREE_TO_RADIAN: Math.PI / 180, TWOPI: Math.PI * 2, getPerpendiculars: function(normal) { var p1 = this.getPerpendicular( normal ); var p2 = normal.cross( p1 ); p2.normalize(); return [ p1, p2 ]; }, getPerpendicular: function( v ) { if( v.x == 0 ) { return new THREE.Vector3D( 1, 0, 0 ); } else { var temp = new THREE.Vector3( v.y, -v.x, 0 ); return temp.normalize(); } } };