/**
* @fileoverview phoria - 3D Entity objects. Base class for chained matrix operations. Concrete Entity implementations.
* @author Kevin Roast
* @date 13th April
*
* @module Entity
*/
(function() {
"use strict";
/**
* BaseEntity is the base that other Entity prototypes extend from. Provides functions to perform chained matrix
* operations and maintains the child entity list. It also provides the onScene event handler functions.
* @class BaseEntity
*/
Phoria.BaseEntity = function()
{
// the model matrix for this object - live manipulation functions below
this.matrix = mat4.create();
this.children = [];
return this;
};
/**
* Factory create method - object literal Entity descripton:
* {
* id: string,
* matrix: mat4,
* children: [...],
* onBeforeScene: function() {...},
* onScene: function() {...},
* disabled: boolean
* }
* @method create
* @param desc {Object} description
* @param e {Phoria.Entity} entity
*/
Phoria.BaseEntity.create = function create(desc, e)
{
// merge structures to generate entity
if (!e) e = new Phoria.BaseEntity();
if (desc.id) e.id = desc.id;
if (desc.matrix) e.matrix = desc.matrix;
if (desc.children) e.children = desc.children;
if (desc.onBeforeScene) e.onBeforeScene(desc.onBeforeScene);
if (desc.onScene) e.onScene(desc.onScene);
if (desc.disabled !== undefined) e.disabled = desc.disabled;
return e;
};
Phoria.BaseEntity.prototype =
{
// {string} optional unique ID for direct look-up of entity during event handlers etc.
id: null,
// {Array} child objects for the purposes of affine transformations - parent matrix applied first
// the child objects themselves can of course have further child objects
children: null,
// {mat4} matrix to be applied to the entity during scene processing
matrix: null,
// {boolean} set to true to disable processing of the Entity and all child entities during the modelView pipeline
disabled: false,
onBeforeSceneHandlers: null,
onSceneHandlers: null,
/**
* Add an onBeforeSceneHandlers event handler function to the entity. Called at the start of each scene
* processing cycle before the local matrix has been multipled by the parent matrix.
*
* @param fn {function} onBeforeSceneHandlers handler signature: function(Phoria.Scene, time) this = Phoria.Entity,
* accepts [] of functions also
*/
onBeforeScene: function onBeforeScene(fn)
{
if (this.onBeforeSceneHandlers === null) this.onBeforeSceneHandlers = [];
this.onBeforeSceneHandlers = this.onBeforeSceneHandlers.concat(fn);
},
/**
* Add an onScene event handler function to the entity. Called at the start of each scene processing cycle after the
* local matrix has been multiplied by the parent matrix.
*
* @param fn {function} onScene handler signature: function(Phoria.Scene, matLocal, time) this = Phoria.Entity,
* accepts [] of functions also
*/
onScene: function onScene(fn)
{
if (this.onSceneHandlers === null) this.onSceneHandlers = [];
this.onSceneHandlers = this.onSceneHandlers.concat(fn);
},
identity: function identity()
{
mat4.identity(this.matrix);
return this;
},
invert: function invert()
{
mat4.invert(this.matrix, this.matrix);
return this;
},
multiply: function multiply(m)
{
mat4.multiply(this.matrix, this.matrix, m);
return this;
},
scale: function scale(vec)
{
mat4.scale(this.matrix, this.matrix, vec);
return this;
},
scaleN: function scale(n)
{
mat4.scale(this.matrix, this.matrix, vec3.fromValues(n,n,n));
return this;
},
rotate: function rotate(rad, axis)
{
mat4.rotate(this.matrix, this.matrix, rad, axis);
return this;
},
rotateX: function rotateX(rad)
{
mat4.rotateX(this.matrix, this.matrix, rad);
return this;
},
rotateY: function rotateY(rad)
{
mat4.rotateY(this.matrix, this.matrix, rad);
return this;
},
rotateZ: function rotateZ(rad)
{
mat4.rotateZ(this.matrix, this.matrix, rad);
return this;
},
/**
* Rotate entity matrix by the given yaw (heading), pitch (elevation) and roll (bank) Euler angles.
* @param {Number} yaw the yaw/heading angle in radians
* @param {Number} pitch the pitch/elevation angle in radians
* @param {Number} roll the roll/bank angle in radians
*/
rotateYPR: function rotateYPR(yaw, pitch, roll)
{
var m = mat4.fromYPR(yaw, pitch, roll);
mat4.multiply(this.matrix, this.matrix, m);
},
translate: function translate(vec)
{
mat4.translate(this.matrix, this.matrix, vec);
return this;
},
translateX: function translateX(n)
{
mat4.translate(this.matrix, this.matrix, vec3.fromValues(n,0,0));
return this;
},
translateY: function translateY(n)
{
mat4.translate(this.matrix, this.matrix, vec3.fromValues(0,n,0));
return this;
},
translateZ: function translateZ(n)
{
mat4.translate(this.matrix, this.matrix, vec3.fromValues(0,0,n));
return this;
},
determinant: function determinant()
{
return mat4.determinant(this.matrix);
},
transpose: function transpose()
{
mat4.transpose(this.matrix, this.matrix);
return this;
}
};
})();
Phoria.CLIP_ARRAY_TYPE = (typeof Uint32Array !== 'undefined') ? Uint32Array : Array;
(function() {
"use strict";
/**
* Entity is the main Phoria 3D object class. It describes the vertices, edges, polygons and textures for a object
* that can be rendered within a scene. Other classes sub-class this to provide more specialised entities such as
* lights or Physics objects. The Entity also descibes a style structure that has a number of configuration settings
* for different types and modes of rendering a 3D object.
* @class Entity
*/
Phoria.Entity = function()
{
Phoria.Entity.superclass.constructor.call(this);
this.points = [];
this.edges = [];
this.polygons = [];
this.textures = [];
this.style = Phoria.Entity.createStyle();
return this;
};
/**
* <pre>
* Factory create method - object literal Entity descripton:
* {
* points: [{x:0,y:0,z:0},...],
* edges: [{a:0,b:1},...],
* polygons: [{vertices:[7,8,10,9]},{vertices:[0,1,2],texture:0,uvs:[0,0,0.5,0.5,0.5,0]},...],
* style: {
* color: [128,128,128], // RGB colour of the object surface
* specular: 0, // if not zero, specifies specular shinyness power - e.g. values like 16 or 64
* diffuse: 1.0, // material diffusion generally ranges from 0-1
* emit: 0.0, // material emission (glow) 0-1
* opacity: 1.0, // material opacity 0-1
* drawmode: "solid", // one of "point", "wireframe", "solid"
* shademode: "lightsource", // one of "plain", "lightsource", "sprite", "callback" (only for point rendering)
* fillmode: "inflate", // one of "fill", "filltwice", "inflate", "fillstroke", "hiddenline"
* objectsortmode: "sorted", // coarse object sort - one of "sorted", "front", "back"
* geometrysortmode: "automatic", // point, edge or polygon sorting mode - one of "sorted", "automatic", "none"
* linewidth: 1.0, // wireframe line thickness
* linescale: 0.0, // depth based scaling factor for wireframes - can be zero for no scaling
* doublesided: false, // true to always render polygons - i.e. do not perform hidden surface test
* texture: undefined // default texture index to use for polygons if not specified - e.g. when UVs are used
* },
* onRender: function() {...}
* }
* </pre>
* @method create
* @param desc {Object}
* @param e {Phoria.Entity} optional entity
*
* @return entity
*/
Phoria.Entity.create = function create(desc, e)
{
// merge structures to generate entity
if (!e) e = new Phoria.Entity();
Phoria.BaseEntity.create(desc, e);
if (desc.points) e.points = desc.points;
if (desc.polygons) e.polygons = desc.polygons;
if (desc.edges) e.edges = desc.edges;
if (desc.style) Phoria.Util.combine(e.style, desc.style);
if (desc.onRender) e.onRender(desc.onRender);
// generate normals - can call generate...() if manually changing points/polys at runtime
e.generatePolygonNormals();
// TODO: apply when gouraud shading for software rendering is added
//e.generateVertexNormals();
return e;
};
/**
* Static helper to construct a default style object with all values populated.
* @method createStyle
* @param s {Object} Optional style object literal to merge into the default style.
* @return style
*/
Phoria.Entity.createStyle = function createStyle(s)
{
var style = {
color: [128,128,128],
diffuse: 1.0,
specular: 0,
drawmode: "solid",
shademode: "lightsource",
fillmode: "inflate",
objectsortmode: "sorted",
geometrysortmode: "automatic",
linewidth: 1.0,
linescale: 0.0,
opacity: 1.0,
doublesided: false
};
if (s) Phoria.Util.combine(style, s);
return style;
};
Phoria.Util.extend(Phoria.Entity, Phoria.BaseEntity, {
// {Array} list of {x:n,y:n,z:n} tuples describing the vertices of the entity
points: null,
// {Array} list of {a:n,b:n} objects describes the wireframe edges of the entity
edges: null,
// {Array} list of {vertices:[n,n,n,...],color:{r,g,b},texture:n} vertices array (minimum 3 per polygon) and
// optional polygon color rgb tuple and optional texture index into the entity textures image list
polygons: null,
// {Object} style description for the entity - merged with the default style as defined in the constructor
style: null,
// {Array} list of texture images available to polygons
textures: null,
onRenderHandlers: null,
_worldcoords: null,
_cameracoords: null,
_coords: null,
_clip: null,
_averagez: 0,
_sorted: true,
/**
* Add an onRender event handler function to the entity. Called if shademode="callback" for custom rendering.
*
* @method onRender
* @param fn {function} onRender handler signature: function(ctx, x, y, w) this = Phoria.Entity,
* accepts [] of functions also
*/
onRender: function onRender(fn)
{
if (this.onRenderHandlers === null) this.onRenderHandlers = [];
this.onRenderHandlers = this.onRenderHandlers.concat(fn);
},
/**
* Calculate and store the face normals for the entity
*
* @method generatePolygonNormals
*/
generatePolygonNormals: function generatePolygonNormals()
{
if (this.polygons)
{
// calculate normal vectors for face data - and set default colour
// value if not supplied in the data set
var points = this.points,
polygons = this.polygons;
for (var i=0, vertices, x1, y1, z1, x2, y2, z2; i<polygons.length; i++)
{
// First calculate normals from 3 points on the poly:
// Vector 1 = Vertex B - Vertex A
// Vector 2 = Vertex C - Vertex A
vertices = polygons[i].vertices;
x1 = points[vertices[1]].x - points[vertices[0]].x;
y1 = points[vertices[1]].y - points[vertices[0]].y;
z1 = points[vertices[1]].z - points[vertices[0]].z;
x2 = points[vertices[2]].x - points[vertices[0]].x;
y2 = points[vertices[2]].y - points[vertices[0]].y;
z2 = points[vertices[2]].z - points[vertices[0]].z;
// save the vec4 normal vector as part of the polygon data structure
polygons[i].normal = Phoria.Util.calcNormalVector(x1, y1, z1, x2, y2, z2);
}
}
},
/**
* Init all the buffers needed by the entity during scene pipeline processing.
* Buffers are re-allocated if the number of coordinates in the entity changes.
*
* @method initCoordinateBuffers
*/
initCoordinateBuffers: function initCoordinateBuffers()
{
var len = this.points.length;
if (this._worldcoords === null || this._worldcoords.length < len)
{
this._worldcoords = new Array(len);
for (var i=0; i<len; i++) this._worldcoords[i] = vec4.create();
}
if (this._cameracoords === null || this._cameracoords.length < len)
{
this._cameracoords = new Array(len);
for (var i=0; i<len; i++) this._cameracoords[i] = vec4.create();
}
if (this._coords === null || this._coords.length < len)
{
this._coords = new Array(len);
for (var i=0; i<len; i++) this._coords[i] = vec4.create();
}
if (this._clip === null || this._clip.length < len)
{
this._clip = new Phoria.CLIP_ARRAY_TYPE(len);
}
},
/**
* Return an object describing the bounding rectangle coordinates of the renderable object in screen coordinates.
* @method getScreenBounds
*
* @return an object with properties; minx, miny, maxx, maxy
*/
getScreenBounds: function getScreenBounds()
{
var minx=10000,miny=10000,maxx=-10000,maxy=-10000;
for (var i=0,p; i<this._coords.length; i++)
{
p = this._coords[i];
if (p[0] < minx) minx = p[0];
if (p[0] > maxx) maxx = p[0];
if (p[1] < miny) miny = p[1];
if (p[1] > maxy) maxy = p[1];
}
return {
minx: minx,
miny: miny,
maxx: maxx,
maxy: maxy
};
},
/**
* Return an object describing the bounding cube coordinates of the entity in world coordinates.
*
* @method getWorldBounds
* @return an object with properties; minx, miny, minz, maxx, maxy, maxz
*/
getWorldBounds: function getWorldBounds()
{
var minx=10000,miny=10000,minz=10000,maxx=-10000,maxy=-10000,maxz=-10000;
for (var i=0,p; i<this._worldcoords.length; i++)
{
p = this._worldcoords[i];
if (p[0] < minx) minx = p[0];
if (p[0] > maxx) maxx = p[0];
if (p[1] < miny) miny = p[1];
if (p[1] > maxy) maxy = p[1];
if (p[2] < minz) minz = p[2];
if (p[2] > maxz) maxz = p[2];
}
return {
minx: minx,
miny: miny,
maxx: maxx,
maxy: maxy,
minz: minz,
maxz: maxz
};
}
});
/**
* Add debug information to an entity.
* @method debug
* @param {Phoria.Entity} entity entity you want to debug
* @param {Object} config debug config options:
* <pre>
* {
* showId: boolean
* showAxis: boolean
* showPosition: boolean
* }
* </pre>
*/
Phoria.Entity.debug = function debug(entity, config)
{
// search child list for debug entity
var id = "Phoria.Debug" + (entity.id ? (" "+entity.id) : "");
var debugEntity = null;
for (var i=0; i<entity.children.length; i++)
{
if (entity.children[i].id === id)
{
debugEntity = entity.children[i];
break;
}
}
// create debug entity if it does not exist
if (debugEntity === null)
{
// add a child entity with a custom renderer - that renders text of the parent id at position
debugEntity = new Phoria.Entity();
debugEntity.id = id;
debugEntity.points = [ {x:0,y:0,z:0} ];
debugEntity.style = {
drawmode: "point",
shademode: "callback",
geometrysortmode: "none",
objectsortmode: "front" // force render on-top of everything else
};
// config object - will be combined with input later
debugEntity.config = {};
debugEntity.onRender(function(ctx, x, y) {
// render debug text
ctx.fillStyle = "#333";
ctx.font = "14pt Helvetica";
var textPos = y;
if (this.config.showId)
{
ctx.fillText(entity.id ? entity.id : "unknown - set Entity 'id' property", x, textPos);
textPos += 16;
}
if (this.config.showPosition)
{
var p = entity.worldposition ? entity.worldposition : debugEntity._worldcoords[0];
ctx.fillText("{x:" + p[0].toFixed(2) + ", y:" + p[1].toFixed(2) + ", z:" + p[2].toFixed(2) + "}", x, textPos);
}
});
entity.children.push(debugEntity);
// add visible axis geometry (lines) as children of entity for showAxis
var fnCreateAxis = function(letter, vector, color) {
var axisEntity = new Phoria.Entity();
axisEntity.points = [ {x:0,y:0,z:0}, {x:2*vector[0],y:2*vector[1],z:2*vector[2]} ];
axisEntity.edges = [ {a:0,b:1} ];
axisEntity.style = {
drawmode: "wireframe",
shademode: "plain",
geometrysortmode: "none",
objectsortmode: "front",
linewidth: 2.0,
color: color
};
axisEntity.disabled = true;
return axisEntity;
};
debugEntity.children.push(fnCreateAxis("X", vec3.fromValues(1,0,0), [255,0,0]));
debugEntity.children.push(fnCreateAxis("Y", vec3.fromValues(0,1,0), [0,255,0]));
debugEntity.children.push(fnCreateAxis("Z", vec3.fromValues(0,0,1), [0,0,255]));
}
// set the config
Phoria.Util.combine(debugEntity.config, config);
for (var i=0; i<debugEntity.children.length; i++)
{
debugEntity.children[i].disabled = !debugEntity.config.showAxis;
}
}
})();
(function() {
"use strict";
Phoria.PositionalAspect = {};
/**
* The PositionalAspect has defines a prototype for objects that may not be rendered directly (i.e. do not need
* to have a visible entity) but do represent a position in the scene.
*
* Augment a prototype with this aspect to provide an easy way to keep track of a it's position in the scene after
* matrix transformations have occured. Examine worldposition at runtime (ensure not null) to get current position.
*
* Set the initial position on object construction if the entity is not positioned at the origin by default.
*/
Phoria.PositionalAspect.prototype =
{
// {xyz} the position of the entity
position: null,
// {vec4} the transformed world position of the entity
worldposition: null,
updatePosition: function updatePosition(matLocal)
{
// update worldposition position of emitter by local transformation -> world
var vec = vec4.fromXYZ(this.position, 1);
vec4.transformMat4(vec, vec, matLocal);
this.worldposition = vec;
}
};
})();
(function() {
"use strict";
/**
* PhysicsEntity builds on the basic entity class to provide very basic physics support. The entity maintains
* a position and a velocity that can be manipulated via constant and impulse forces. It also optionally
* applies gravity. After the physics calculations the entity matrix is updated to the new position.
* @class PhysicsEntity
*/
Phoria.PhysicsEntity = function()
{
Phoria.PhysicsEntity.superclass.constructor.call(this);
this.velocity = {x:0, y:0, z:0};
this.position = {x:0, y:0, z:0};
this._force = {x:0, y:0, z:0};
this._acceleration = null;
this.gravity = true;
// add handlers to apply physics etc.
this.onBeforeScene(this.applyPhysics);
this.onScene(this.transformToScene);
return this;
};
/**
*<pre>
* Factory create method - object literal Entity descripton:
* {
* velocity: {x:0,y:0,z:0},
* position: {x:0,y:0,z:0}, // NOTE: position is not render data - just informational for scene callbacks etc.
* force: {x:0,y:0,z:0},
* gravity: boolean
* }</pre>
* @method create
* @param desc
* @return e entity
*/
Phoria.PhysicsEntity.create = function create(desc)
{
// merge structures to generate entity
var e = new Phoria.PhysicsEntity();
Phoria.Entity.create(desc, e);
if (desc.velocity) e.velocity = desc.velocity;
if (desc.position) e.position = desc.position;
if (desc.force) e._force = desc.force;
if (desc.gravity !== undefined) e.gravity = desc.gravity;
return e;
};
Phoria.Util.extend(Phoria.PhysicsEntity, Phoria.Entity, {
// {xyz} current velocity of the entity
velocity: null,
// {boolean} true to automatically apply gravity force to the object, false otherwise
gravity: false,
_force: null,
_acceleration: null,
/**
* Apply an impluse force to the entity
* @param f {Object} xyz tuple for the force direction
*/
impulse: function impulse(f)
{
this._acceleration = f;
},
/**
* Apply a constant force to the entity
* @param f {Object} xyz tuple for the force direction
*/
force: function force(f)
{
this._force = f;
},
/**
* Scene handler to apply basic physics to the entity.
* Current velocity is updated by any acceleration that is set, by any constant
* force that is set and also optionally by fixed gravity.
*/
applyPhysics: function applyPhysics(scene)
{
/**
* NOTE: Physics simulation is updated in real-time regardless of the FPS of
* the rest of the animation - set to ideal time (in secs) to avoid glitches
*/
var time = 1000/60/1000; // 60FPS in seconds
var tt = time * time;
// apply impulse force if set then reset it to none
if (this._acceleration)
{
this.velocity.x += (this._acceleration.x * tt);
this.velocity.y += (this._acceleration.y * tt);
this.velocity.z += (this._acceleration.z * tt);
this._acceleration = null;
}
// apply constant force
if (this._force)
{
this.velocity.x += (this._force.x * tt);
this.velocity.y += (this._force.y * tt);
this.velocity.z += (this._force.z * tt);
}
// apply constant gravity force if activated
if (this.gravity)
{
this.velocity.x += (Phoria.PhysicsEntity.GRAVITY.x * tt);
this.velocity.y += (Phoria.PhysicsEntity.GRAVITY.y * tt);
this.velocity.z += (Phoria.PhysicsEntity.GRAVITY.z * tt);
}
// apply current velocity to position
this.translate(vec3.fromXYZ(this.velocity));
},
transformToScene: function transformToScene(scene, matLocal)
{
// local transformation -> world
this.updatePosition(matLocal);
}
});
Phoria.Util.augment(Phoria.PhysicsEntity, Phoria.PositionalAspect);
})();
/**
* Constants
*/
Phoria.PhysicsEntity.GRAVITY = {x:0, y:-9.8, z:0};
(function() {
"use strict";
/**
* Emitter is used to generate "particle" physics entities at a given rate per second with a flexible configuration
* of velocity and position starting point. The emitter itself is not rendered, but exposes a style config that is
* applied to all child particle entities. An event handler 'onParticle' is provided to allow further customisation
* of particles as they are generated.
* @class EmitterEntity
*/
Phoria.EmitterEntity = function()
{
Phoria.EmitterEntity.superclass.constructor.call(this);
this.position = {x:0,y:0,z:0};
this.positionRnd = {x:0,y:0,z:0};
this.velocity = {x:0,y:1,z:0};
this.velocityRnd = {x:0,y:0,z:0};
this.maximum = 1000;
this.gravity = true;
// default particle rendering style
var style = Phoria.Entity.createStyle();
style.drawmode = "point";
style.shademode = "plain";
style.geometrysortmode = "none";
style.linewidth = 5;
style.linescale = 2;
this.style = style;
this.textures = [];
this._lastEmitTime = Date.now();
// add handler to emit particles
this.onScene(this.emitParticles);
return this;
};
/**
* <pre>
* Factory create method - object literal Entity descripton:
* {
* position: {x:0,y:0,z:0}, // used as the start position for particles - default (0,0,0)
* positionRnd: {x:0,y:0,z:0}, // randomness to apply to the start position - default (0,0,0)
* rate: Number, // particles per second to emit - default 0
* maximum: Number, // maximum allowed particles (zero for unlimited) - default 1000
* velocity: {x:0,y:1,z:0}, // start velocity of the particle - default (0,1,0)
* velocityRnd: {x:0,y:0,z:0}, // randomness to apply to the velocity - default (0,0,0)
* lifetime: Number, // lifetime in ms of the particle (zero for unlimited) - default 0
* lifetimeRnd: Number, // lifetime randomness to apply - default 0
* gravity: boolean // true to apply gravity to particles - default true
* style: {...} // particle rendering style (@see Phoria.Entity)
* onParticle: function() {...}// particle create callback function
* }
* </pre>
* @method create
* @param {Object} desc description
* @return {Phoria.Entity} entity
*/
Phoria.EmitterEntity.create = function create(desc)
{
// TODO: provide an emitter() callback function - which could be used to apply velocity or whatever
// rather than assuming all particle generation will use the parameters below
// merge structures to generate entity
var e = new Phoria.EmitterEntity();
Phoria.BaseEntity.create(desc, e);
if (desc.position) e.position = desc.position;
if (desc.positionRnd) e.positionRnd = desc.positionRnd;
if (desc.rate) e.rate = desc.rate;
if (desc.maximum) e.maximum = desc.maximum;
if (desc.velocity) e.velocity = desc.velocity;
if (desc.velocityRnd) e.velocityRnd = desc.velocityRnd;
if (desc.lifetime) e.lifetime = desc.lifetime;
if (desc.lifetimeRnd) e.lifetimeRnd = desc.lifetimeRnd;
if (desc.gravity !== undefined) e.gravity = desc.gravity;
if (desc.style) Phoria.Util.combine(e.style, desc.style);
if (desc.onParticle) e.onParticle(desc.onParticle);
return e;
};
Phoria.Util.extend(Phoria.EmitterEntity, Phoria.BaseEntity, {
// {Object} style description for the entity - merged with the default style as defined in the constructor
style: null,
// {Number} output rate of the emitter in items per second
rate: 0,
// {Number} optional maximum number of particles allowed as children of the emitter
maximum: 0,
// {xyz} start velocity of the particles
velocity: null,
// {xyz} randomness to apply to the start velocity to particles
velocityRnd: null,
// {Number} lifetime of the particles in miliseconds
lifetime: 0,
// {Number} randomness to apply to the lifetime of the particles
lifetimeRnd: 0,
// {boolean} true to automatically apply gravity force to the particles, false otherwise
gravity: false,
_lastEmitTime: 0,
onParticleHandlers: null,
/**
* Add an onParticle event handler function to the entity. Typically used to decorate or modify a particle
* before it is added to the emitter child list and begins it's lifecycle.
*
* @param fn {function} onParticle handler signature: function(particle) this = Phoria.EmitterEntity,
* accepts [] of functions also
*/
onParticle: function onParticle(fn)
{
if (this.onParticleHandlers === null) this.onParticleHandlers = [];
this.onParticleHandlers = this.onParticleHandlers.concat(fn);
},
/**
* Scene handler to generate child particles from the emitter.
*/
emitParticles: function emitParticles(scene, matLocal, time)
{
// update worldposition position of emitter by local transformation -> world
this.updatePosition(matLocal);
// TODO: currently this assumes all direct children of the emitter are particles
// if they are not - this calculation needs to be changed to keep track.
// clean up expired particles - based on lifetime
var now = Date.now();
for (var i=0, p; i<this.children.length; i++)
{
p = this.children[i];
if (p._gravetime && now > p._gravetime)
{
// found a particle to remove
this.children.splice(i, 1);
}
}
// emit particle objects
var since = now - this._lastEmitTime;
var count = Math.floor((this.rate / 1000) * since);
if (count > 0)
{
// emit up to count value - also checking maximum to ensure total particle count is met
for (var c=0; c<count && (this.maximum === 0 || this.children.length < this.maximum); c++)
{
var pos = {x:this.position.x, y:this.position.y, z:this.position.z};
pos.x += (Math.random() * this.positionRnd.x) - (this.positionRnd.x * 0.5);
pos.y += (Math.random() * this.positionRnd.y) - (this.positionRnd.y * 0.5);
pos.z += (Math.random() * this.positionRnd.z) - (this.positionRnd.z * 0.5);
var vel = {x:this.velocity.x, y:this.velocity.y, z:this.velocity.z};
vel.x += (Math.random() * this.velocityRnd.x) - (this.velocityRnd.x * 0.5);
vel.y += (Math.random() * this.velocityRnd.y) - (this.velocityRnd.y * 0.5);
vel.z += (Math.random() * this.velocityRnd.z) - (this.velocityRnd.z * 0.5);
// create particle directly - avoid overhead of the more friendly factory method
var particle = new Phoria.PhysicsEntity();
particle.position = pos;
particle.points = [ pos ];
particle.velocity = vel;
particle.gravity = this.gravity;
particle.style = this.style;
particle.textures = this.textures;
if (this.lifetime !== 0)
{
particle._gravetime = Math.floor(now + this.lifetime + (this.lifetimeRnd * Math.random()) - this.lifetimeRnd*0.5);
}
// execute any callbacks interested in the particle creation
if (this.onParticleHandlers !== null)
{
for (var h=0; h<this.onParticleHandlers.length; h++)
{
this.onParticleHandlers[h].call(this, particle);
}
}
this.children.push(particle);
}
this._lastEmitTime = now;
}
}
});
Phoria.Util.augment(Phoria.EmitterEntity, Phoria.PositionalAspect);
})();
(function() {
"use strict";
/**
* BaseLight is the base that the Light classes extend from. Provides RGB color and light intensity properties.
*
* @class BaseLight
*/
Phoria.BaseLight = function()
{
Phoria.BaseLight.superclass.constructor.call(this);
this.color = [1.0, 1.0, 1.0];
this.intensity = 1.0;
return this;
};
Phoria.Util.extend(Phoria.BaseLight, Phoria.BaseEntity, {
// [r,g,b] - note! light colour component levels are specified from 0.0 - 1.0
color: null,
// {Number} light intensity typically between 0-1
intensity: 0.0
});
})();
(function() {
"use strict";
/**
* DistantLight models an infinitely distant light that has no position only a normalised direction from which light eminates.
*
* @class DistantLight
*/
Phoria.DistantLight = function()
{
Phoria.DistantLight.superclass.constructor.call(this);
// direction should be a normalised vector
this.direction = {x:0, y:0, z:1};
// add scene handler to transform the light direction into world direction
this.onScene(this.transformToScene);
return this;
};
/**
* Factory create method - object literal Light descripton
* @method create
* @param {Object} desc description
*
* @return {Phoria.Entity} distant light entity
*/
Phoria.DistantLight.create = function create(desc)
{
// merge structures to generate entity
var e = new Phoria.DistantLight();
Phoria.BaseEntity.create(desc, e);
if (desc.color) e.color = desc.color;
if (desc.intensity) e.intensity = desc.intensity;
if (desc.direction) e.direction = vec3.toXYZ(vec3.normalize(e.direction, vec3.fromXYZ(desc.direction)));
return e;
};
Phoria.Util.extend(Phoria.DistantLight, Phoria.BaseLight, {
// light direction
direction: null,
worlddirection: null,
transformToScene: function transformToScene()
{
this.worlddirection = vec3.fromValues(
-this.direction.x,
-this.direction.y,
-this.direction.z);
}
});
})();
(function() {
"use strict";
/**
* PointLight models a light that has a position within the scene and from which light eminates in all directions
* equally. These lights also have an attenuation which describes how the light falls off over distance. A number of
* attentuation types are provided such as none (no fall-off over distance), linear (fall-off directly related to the
* distance from the light) and squared (fall-off related to distance squared).
*
* @class PointLight
*/
Phoria.PointLight = function()
{
Phoria.PointLight.superclass.constructor.call(this);
this.position = {x: 0, y:0, z:-1};
this.attenuation = 0.1;
this.attenuationFactor = "linear";
// add scene handler to transform the light position into world position
this.onScene(this.transformToScene);
return this;
};
/**
* <pre>
* Factory create method - object literal Light description
* {
* position: {x:0,y:0,z:0},
* color: [0-1,0-1,0-1],
* intensity: 0-1,
* attenuation: 0-1,
* attenuationFactor: "none"|"linear"|"squared"
* }
* </pre>
*
* @method create
* @param {Object} desc description
* @return {Phoria.Entity} point light entity
*/
Phoria.PointLight.create = function create(desc)
{
// merge structures to generate entity
var e = new Phoria.PointLight();
Phoria.BaseEntity.create(desc, e);
if (desc.color) e.color = desc.color;
if (desc.intensity) e.intensity = desc.intensity;
if (desc.position) e.position = desc.position;
if (desc.attenuation) e.attenuation = desc.attenuation;
if (desc.attenuationFactor) e.attenuationFactor = desc.attenuationFactor;
return e;
};
Phoria.Util.extend(Phoria.PointLight, Phoria.BaseLight, {
// falloff
attenuation: 0,
attenuationFactor: null,
transformToScene: function transformToScene(scene, matLocal, time)
{
// update worldposition position of light by local transformation -> world
this.updatePosition(matLocal);
}
});
Phoria.Util.augment(Phoria.PointLight, Phoria.PositionalAspect);
})();