/*!
* Crafty v0.4.9
* http://craftyjs.com
*
* Copyright 2010, Louis Stowasser
* Dual licensed under the MIT or GPL licenses.
*/
(function (window, undefined) {
/**@
* #Crafty
* @category Core
* Select a set of or single entities by components or an entity's ID.
*
* Crafty uses syntax similar to jQuery by having a selector engine to select entities by their components.
*
* @example
* ~~~
* Crafty("MyComponent")
* Crafty("Hello 2D Component")
* Crafty("Hello, 2D, Component")
* ~~~
*
* The first selector will return all entities that has the component `MyComponent`. The second will return all entities that has `Hello` and `2D` and `Component` whereas the last will return all entities that has at least one of those components (or).
* ~~~
* Crafty(1)
* ~~~
* Passing an integer will select the entity with that `ID`.
*
* Finding out the `ID` of an entity can be done by returning the property `0`.
* ~~~
* var ent = Crafty.e("2D");
* ent[0]; //ID
* ~~~
*/
var Crafty = function (selector) {
return new Crafty.fn.init(selector);
},
GUID = 1, //GUID for entity IDs
FPS = 25,
frame = 1,
components = {}, //map of components and their functions
entities = {}, //map of entities and their data
handlers = {}, //global event handlers
onloads = [], //temporary storage of onload handlers
tick,
/*
* `window.requestAnimationFrame` or its variants is called for animation.
* `.requestID` keeps a record of the return value previous `window.requestAnimationFrame` call.
* This is an internal variable. Used to stop frame.
*/
requestID,
noSetter,
loops = 0,
milliSecPerFrame = 1000 / FPS,
nextGameTick = (new Date).getTime(),
slice = Array.prototype.slice,
rlist = /\s*,\s*/,
rspace = /\s+/;
/**@
* #Crafty Core
* @category Core
* @trigger NewEntityName - After setting new name for entity - String - entity name
* @trigger NewComponent - when a new component is added to the entity - String - Component
* @trigger RemoveComponent - when a component is removed from the entity - String - Component
* @trigger Remove - when the entity is removed by calling .destroy()
*
* Set of methods added to every single entity.
*/
Crafty.fn = Crafty.prototype = {
init: function (selector) {
//select entities by component
if (typeof selector === "string") {
var elem = 0, //index elements
e, //entity forEach
current,
and = false, //flags for multiple
or = false,
del,
comps,
score,
i, l;
if (selector === '*') {
for (e in entities) {
this[+e] = entities[e];
elem++;
}
this.length = elem;
return this;
}
//multiple components OR
if (selector.indexOf(',') !== -1) {
or = true;
del = rlist;
//deal with multiple components AND
}
else if (selector.indexOf(' ') !== -1) {
and = true;
del = rspace;
}
//loop over entities
for (e in entities) {
if (!entities.hasOwnProperty(e)) continue; //skip
current = entities[e];
if (and || or) { //multiple components
comps = selector.split(del);
i = 0;
l = comps.length;
score = 0;
for (; i < l; i++) //loop over components
if (current.__c[comps[i]]) score++; //if component exists add to score
//if anded comps and has all OR ored comps and at least 1
if (and && score === l || or && score > 0) this[elem++] = +e;
} else if (current.__c[selector]) this[elem++] = +e; //convert to int
}
//extend all common components
if (elem > 0 && !and && !or) this.extend(components[selector]);
if (comps && and) for (i = 0; i < l; i++) this.extend(components[comps[i]]);
this.length = elem; //length is the last index (already incremented)
} else { //Select a specific entity
if (!selector) { //nothin passed creates God entity
selector = 0;
if (!(selector in entities)) entities[selector] = this;
}
//if not exists, return undefined
if (!(selector in entities)) {
this.length = 0;
return this;
}
this[0] = selector;
this.length = 1;
//update from the cache
if (!this.__c) this.__c = {};
//update to the cache if NULL
if (!entities[selector]) entities[selector] = this;
return entities[selector]; //return the cached selector
}
return this;
},
/**@
* #.setName
* @comp Crafty Core
* @sign public this .setName(String name)
* @param name - A human readable name for debugging purposes.
*
* @example
* ~~~
* this.setName("Player");
* ~~~
*/
setName: function (name) {
var entityName = String(name);
this._entityName = entityName;
this.trigger("NewEntityName", entityName);
return this;
},
/**@
* #.addComponent
* @comp Crafty Core
* @sign public this .addComponent(String componentList)
* @param componentList - A string of components to add seperated by a comma `,`
* @sign public this .addComponent(String Component1[, .., String ComponentN])
* @param Component# - Component ID to add.
* Adds a component to the selected entities or entity.
*
* Components are used to extend the functionality of entities.
* This means it will copy properties and assign methods to
* augment the functionality of the entity.
*
* There are multiple methods of adding components. Passing a
* string with a list of component names or passing multiple
* arguments with the component names.
*
* If the component has a function named `init` it will be called.
*
* @example
* ~~~
* this.addComponent("2D, Canvas");
* this.addComponent("2D", "Canvas");
* ~~~
*/
addComponent: function(id) {
var uninit = [], c = 0, ul, //array of components to init
i = 0, l, comps;
//add multiple arguments
if (arguments.length > 1) {
l = arguments.length;
for (; i < l; i++) {
this.__c[arguments[i]] = true;
uninit.push(arguments[i]);
}
//split components if contains comma
} else if (id.indexOf(',') !== -1) {
comps = id.split(rlist);
l = comps.length;
for (; i < l; i++) {
this.__c[comps[i]] = true;
uninit.push(comps[i]);
}
//single component passed
} else {
this.__c[id] = true;
uninit.push(id);
}
//extend the components
ul = uninit.length;
for (; c < ul; c++) {
var comp = components[uninit[c]];
this.extend(comp);
//if constructor, call it
if (comp && "init" in comp) {
comp.init.call(this);
}
}
this.trigger("NewComponent", ul);
return this;
},
/**@
* #.toggleComponent
* @comp Crafty Core
* @sign public this. toggleComponent(String componentID,String componentToggle)
* @param toggle - Component ID to replace instead of remove
* Add or Remove Components
*
* @example
* ~~~
* var e = Crafty.e("2D,DOM,Test");
* e.toggleComponent("Test,Test2"); //Remove Test add Test2 and vice versa
* ~~~
*/
toggleComponent: function(toggle) {
var i = 0, l, comps;
if (arguments.length > 1) {
l = arguments.length;
for (; i < l; i++) {
if(this.has(arguments[i])){
this.removeComponent(arguments[i]);
}
else{
this.addComponent(arguments[i]);
}
}
//split components if contains comma
} else if (toggle.indexOf(',') !== -1) {
comps = toggle.split(rlist);
l = comps.length;
for (; i < l; i++) {
if(this.has(comps[i])){
this.removeComponent(comps[i]);
}else{
this.addComponent(comps[i]);
}
}
//single component passed
} else {
if(this.has(toggle)){
this.removeComponent(toggle);
}else{
this.addComponent(toggle);
}
}
return this;
},
/**@
* #.requires
* @comp Crafty Core
* @sign public this .requires(String componentList)
* @param componentList - List of components that must be added
*
* Makes sure the entity has the components listed. If the entity does not
* have the component, it will add it.
*
* @see .addComponent
*/
requires: function (list) {
var comps = list.split(rlist),
i = 0, l = comps.length,
comp;
//loop over the list of components and add if needed
for (; i < l; ++i) {
comp = comps[i];
if (!this.has(comp)) this.addComponent(comp);
}
return this;
},
/**@
* #.removeComponent
* @comp Crafty Core
* @sign public this .removeComponent(String Component[, soft])
* @param component - Component to remove
* @param soft - Whether to soft remove it (defaults to `true`)
*
* Removes a component from an entity. A soft remove (the default) will only
* refrain `.has()` from returning true. Hard will remove all
* associated properties and methods.
*/
removeComponent: function (id, soft) {
if (soft === false) {
var props = components[id], prop;
for (prop in props) {
delete this[prop];
}
}
delete this.__c[id];
this.trigger("RemoveComponent", id);
return this;
},
/**@
* #.has
* @comp Crafty Core
* @sign public Boolean .has(String component)
* Returns `true` or `false` depending on if the
* entity has the given component.
*
* For better performance, simply use the `.__c` object
* which will be `true` if the entity has the component or
* will not exist (or be `false`).
*/
has: function (id) {
return !!this.__c[id];
},
/**@
* #.attr
* @comp Crafty Core
* @sign public this .attr(String property, * value)
* @param property - Property of the entity to modify
* @param value - Value to set the property to
* @sign public this .attr(Object map)
* @param map - Object where the key is the property to modify and the value as the property value
* @trigger Change - when properties change - {key: value}
*
* Use this method to set any property of the entity.
*
* @example
* ~~~
* this.attr({key: "value", prop: 5});
* this.key; //value
* this.prop; //5
*
* this.attr("key", "newvalue");
* this.key; //newvalue
* ~~~
*/
attr: function (key, value) {
if (arguments.length === 1) {
//if just the key, return the value
if (typeof key === "string") {
return this[key];
}
//extend if object
this.extend(key);
this.trigger("Change", key); //trigger change event
return this;
}
//if key value pair
this[key] = value;
var change = {};
change[key] = value;
this.trigger("Change", change); //trigger change event
return this;
},
/**@
* #.toArray
* @comp Crafty Core
* @sign public this .toArray(void)
*
* This method will simply return the found entities as an array.
*/
toArray: function () {
return slice.call(this, 0);
},
/**@
* #.timeout
* @comp Crafty Core
* @sign public this .timeout(Function callback, Number delay)
* @param callback - Method to execute after given amount of milliseconds
* @param delay - Amount of milliseconds to execute the method
*
* The delay method will execute a function after a given amount of time in milliseconds.
*
* Essentially a wrapper for `setTimeout`.
*
* @example
* Destroy itself after 100 milliseconds
* ~~~
* this.timeout(function() {
this.destroy();
* }, 100);
* ~~~
*/
timeout: function (callback, duration) {
this.each(function () {
var self = this;
setTimeout(function () {
callback.call(self);
}, duration);
});
return this;
},
/**@
* #.bind
* @comp Crafty Core
* @sign public this .bind(String eventName, Function callback)
* @param eventName - Name of the event to bind to
* @param callback - Method to execute when the event is triggered
* Attach the current entity (or entities) to listen for an event.
*
* Callback will be invoked when an event with the event name passed
* is triggered. Depending on the event, some data may be passed
* via an argument to the callback function.
*
* The first argument is the event name (can be anything) whilst the
* second argument is the callback. If the event has data, the
* callback should have an argument.
*
* Events are arbitrary and provide communication between components.
* You can trigger or bind an event even if it doesn't exist yet.
*
* @example
* ~~~
* this.attr("triggers", 0); //set a trigger count
* this.bind("myevent", function() {
* this.triggers++; //whenever myevent is triggered, increment
* });
* this.bind("EnterFrame", function() {
* this.trigger("myevent"); //trigger myevent on every frame
* });
* ~~~
*
* @see .trigger, .unbind
*/
bind: function (event, callback) {
//optimization for 1 entity
if (this.length === 1) {
if (!handlers[event]) handlers[event] = {};
var h = handlers[event];
if (!h[this[0]]) h[this[0]] = []; //init handler array for entity
h[this[0]].push(callback); //add current callback
return this;
}
this.each(function () {
//init event collection
if (!handlers[event]) handlers[event] = {};
var h = handlers[event];
if (!h[this[0]]) h[this[0]] = []; //init handler array for entity
h[this[0]].push(callback); //add current callback
});
return this;
},
/**@
* #.unbind
* @comp Crafty Core
* @sign public this .unbind(String eventName[, Function callback])
* @param eventName - Name of the event to unbind
* @param callback - Function to unbind
* Removes binding with an event from current entity.
*
* Passing an event name will remove all events binded to
* that event. Passing a reference to the callback will
* unbind only that callback.
* @see .bind, .trigger
*/
unbind: function (event, callback) {
this.each(function () {
var hdl = handlers[event], i = 0, l, current;
//if no events, cancel
if (hdl && hdl[this[0]]) l = hdl[this[0]].length;
else return this;
//if no function, delete all
if (!callback) {
delete hdl[this[0]];
return this;
}
//look for a match if the function is passed
for (; i < l; i++) {
current = hdl[this[0]];
if (current[i] == callback) {
current.splice(i, 1);
i--;
}
}
});
return this;
},
/**@
* #.trigger
* @comp Crafty Core
* @sign public this .trigger(String eventName[, Object data])
* @param eventName - Event to trigger
* @param data - Arbitrary data that will be passed into every callback as an argument
* Trigger an event with arbitrary data. Will invoke all callbacks with
* the context (value of `this`) of the current entity object.
*
* *Note: This will only execute callbacks within the current entity, no other entity.*
*
* The first argument is the event name to trigger and the optional
* second argument is the arbitrary event data. This can be absolutely anything.
*/
trigger: function (event, data) {
if (this.length === 1) {
//find the handlers assigned to the event and entity
if (handlers[event] && handlers[event][this[0]]) {
var callbacks = handlers[event][this[0]], i = 0, l = callbacks.length;
for (; i < l; i++) {
callbacks[i].call(this, data);
}
}
return this;
}
this.each(function () {
//find the handlers assigned to the event and entity
if (handlers[event] && handlers[event][this[0]]) {
var callbacks = handlers[event][this[0]], i = 0, l = callbacks.length;
for (; i < l; i++) {
callbacks[i].call(this, data);
}
}
});
return this;
},
/**@
* #.each
* @sign public this .each(Function method)
* @param method - Method to call on each iteration
* Iterates over found entities, calling a function for every entity.
*
* The function will be called for every entity and will pass the index
* in the iteration as an argument. The context (value of `this`) of the
* function will be the current entity in the iteration.
*
* @example
* Destroy every second 2D entity
* ~~~
* Crafty("2D").each(function(i) {
* if(i % 2 === 0) {
* this.destroy();
* }
* });
* ~~~
*/
each: function (func) {
var i = 0, l = this.length;
for (; i < l; i++) {
//skip if not exists
if (!entities[this[i]]) continue;
func.call(entities[this[i]], i);
}
return this;
},
/**@
* #.clone
* @comp Crafty Core
* @sign public Entity .clone(void)
* @returns Cloned entity of the current entity
*
* Method will create another entity with the exact same
* properties, components and methods as the current entity.
*/
clone: function () {
var comps = this.__c,
comp,
prop,
clone = Crafty.e();
for (comp in comps) {
clone.addComponent(comp);
}
for (prop in this) {
if (prop != "0" && prop != "_global" && prop != "_changed" && typeof this[prop] != "function" && typeof this[prop] != "object") {
clone[prop] = this[prop];
}
}
return clone;
},
/**@
* #.setter
* @comp Crafty Core
* @sign public this .setter(String property, Function callback)
* @param property - Property to watch for modification
* @param callback - Method to execute if the property is modified
* Will watch a property waiting for modification and will then invoke the
* given callback when attempting to modify.
*
* *Note: Support in IE<9 is slightly different. The method will be executed
* after the property has been set*
*/
setter: function (prop, callback) {
if (Crafty.support.setter) {
this.__defineSetter__(prop, callback);
} else if (Crafty.support.defineProperty) {
Object.defineProperty(this, prop, {
set: callback,
configurable: true
});
} else {
noSetter.push({
prop: prop,
obj: this,
fn: callback
});
}
return this;
},
/**@
* #.destroy
* @comp Crafty Core
* @sign public this .destroy(void)
* Will remove all event listeners and delete all properties as well as removing from the stage
*/
destroy: function () {
//remove all event handlers, delete from entities
this.each(function () {
this.trigger("Remove");
for (var e in handlers) {
this.unbind(e);
}
delete entities[this[0]];
});
}
};
//give the init instances the Crafty prototype
Crafty.fn.init.prototype = Crafty.fn;
/**
* Extension method to extend the namespace and
* selector instances
*/
Crafty.extend = Crafty.fn.extend = function (obj) {
var target = this, key;
//don't bother with nulls
if (!obj) return target;
for (key in obj) {
if (target === obj[key]) continue; //handle circular reference
target[key] = obj[key];
}
return target;
};
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller
// fixes from Paul Irish and Tino Zijdel
/**@
* #Crafty.extend
* @category Core
* Used to extend the Crafty namespace.
*/
Crafty.extend({
/**@
* #Crafty.init
* @category Core
* @trigger EnterFrame - on each frame - { frame: Number }
* @trigger Load - Just after the viewport is initialised. Before the EnterFrame loops is started
* @sign public this Crafty.init([Number width, Number height])
* @param width - Width of the stage
* @param height - Height of the stage
*
* Create a div with id `cr-stage`, if there is not already an HTMLElement with id `cr-stage` (by `Crafty.viewport.init`).
*
* Starts the `EnterFrame` interval. This will call the `EnterFrame` event for every frame.
*
* Can pass width and height values for the stage otherwise will default to window size (see `Crafty.DOM.window`).
*
* All `Load` events will be executed.
*
* Uses `requestAnimationFrame` to sync the drawing with the browser but will default to `setInterval` if the browser does not support it.
* @see Crafty.stop, Crafty.viewport
*/
init: function (w, h) {
Crafty.viewport.init(w, h);
//call all arbitrary functions attached to onload
this.trigger("Load");
this.timer.init();
return this;
},
/**@
* #.getVersion
* @comp Crafty Core
* @sign public this .getVersion()
* @returns Actualy crafty version
*
* @example
* ~~~
* Crafty.getVersion(); //'0.4.8'
* ~~~
*/
getVersion: function () {
return '0.4.8';
},
/**@
* #Crafty.stop
* @category Core
* @trigger CraftyStop - when the game is stopped
* @sign public this Crafty.stop(void)
*
* Stops the EnterFrame interval and removes the stage element.
*
* To restart, use `Crafty.init()`.
* @see Crafty.init
*/
stop: function () {
this.timer.stop();
Crafty.stage.elem.parentNode.removeChild(Crafty.stage.elem);
return this;
},
/**@
* #Crafty.pause
* @category Core
* @trigger Pause - when the game is paused
* @trigger Unpause - when the game is unpaused
* @sign public this Crafty.pause(void)
*
* Pauses the game by stoping the EnterFrame event from firing. If the game is already paused it is unpaused.
* You can pass a boolean parameter if you want to pause or unpause mo matter what the current state is.
* Modern browsers pauses the game when the page is not visible to the user. If you want the Pause event
* to be triggered when that happens you can enable autoPause in `Crafty.settings`.
*
* @example
* Have an entity pause the game when it is clicked.
* ~~~
* button.bind("click", function() {
* Crafty.pause();
* });
* ~~~
*/
pause: function (toggle) {
if (arguments.length == 1 ? toggle : !this._paused) {
this.trigger('Pause');
this._paused = true;
Crafty.timer.stop();
Crafty.keydown = {};
} else {
this.trigger('Unpause');
this._paused = false;
Crafty.timer.init();
}
return this;
},
/**@
* #Crafty.isPaused
* @category Core
* @sign public this Crafty.isPaused()
*
* Check whether the game is already paused or not.
*
* @example
* ~~~
* Crafty.isPaused();
* ~~~
*/
isPaused: function () {
return this._paused;
},
/**@
* #Crafty.timer
* @category Internal
* Handles game ticks
*/
timer: {
prev: (+new Date),
current: (+new Date),
curTime: Date.now(),
frameTime:0,
frames:0,
frameSkip:10,
init: function () {
var onFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
null,
self=this;
if (onFrame) {
tick = function () {
Crafty.timer.step();
requestID = onFrame(tick);
//console.log(requestID + ', ' + frame)
}
tick();
} else {
tick = setInterval(Crafty.timer.step, milliSecPerFrame);
}
},
stop: function () {
Crafty.trigger("CraftyStop");
if (typeof tick === "number") clearInterval(tick);
var onFrame = window.cancelRequestAnimationFrame ||
window.webkitCancelRequestAnimationFrame ||
window.mozCancelRequestAnimationFrame ||
window.oCancelRequestAnimationFrame ||
window.msCancelRequestAnimationFrame ||
null;
if (onFrame) onFrame(requestID);
tick = null;
},
/**@
* #Crafty.timer.step
* @comp Crafty.timer
* @sign public void Crafty.timer.step()
* Advances the game by triggering `EnterFrame` and calls `Crafty.DrawManager.draw` to update the stage.
*/
step: function () {
loops = 0;
this.curTime = Date.now();
if (this.curTime - nextGameTick > 60 * milliSecPerFrame) {
nextGameTick = this.curTime - milliSecPerFrame;
}
while (this.curTime > nextGameTick && loops < this.frameSkip) {
Crafty.trigger("EnterFrame", { frame: frame++ });
nextGameTick += milliSecPerFrame;
loops++;
}
if (loops > 0) {
var interpolation = parseFloat(Date.now()+milliSecPerFrame- nextGameTick) / parseFloat(milliSecPerFrame);
Crafty.DrawManager.draw(interpolation);
}
if(this.curTime > this.frameTime){
Crafty.trigger("MessureFPS",{value:this.frame});
this.frame = 0;
this.frameTime = this.curTime + 1000;
}else{
this.frame++;
}
},
/**@
* #Crafty.timer.getFPS
* @comp Crafty.timer
* @sign public void Crafty.timer.getFPS()
* Returns the target frames per second. This is not an actual frame rate.
*/
getFPS: function () {
return FPS;
},
setFPS: function (value) {
FPS = value;
milliSecPerFrame = 1000/FPS;
},
/**@
* #Crafty.timer.simulateFrames
* @comp Crafty.timer
* Advances the game state by a number of frames and draws the resulting stage at the end. Useful for tests and debugging.
* @sign public this Crafty.timer.simulateFrames(Number frames)
* @param frames - number of frames to simulate
*/
simulateFrames: function (frames) {
while (frames-- > 0) {
Crafty.trigger("EnterFrame", {
frame: frame++
});
}
Crafty.DrawManager.draw();
}
},
/**@
* #Crafty.e
* @category Core
* @trigger NewEntity - When the entity is created and all components are added - { id:Number }
* @sign public Entity Crafty.e(String componentList)
* @param componentList - List of components to assign to new entity
* @sign public Entity Crafty.e(String component1[, .., String componentN])
* @param component# - Component to add
*
* Creates an entity. Any arguments will be applied in the same
* way `.addComponent()` is applied as a quick way to add components.
*
* Any component added will augment the functionality of
* the created entity by assigning the properties and methods from the component to the entity.
*
* @example
* ~~~
* var myEntity = Crafty.e("2D, DOM, Color");
* ~~~
*
* @see Crafty.c
*/
e: function () {
var id = UID(), craft;
entities[id] = null; //register the space
entities[id] = craft = Crafty(id);
if (arguments.length > 0) {
craft.addComponent.apply(craft, arguments);
}
craft.setName('Entity #'+id); //set default entity human readable name
craft.addComponent("obj"); //every entity automatically assumes obj
Crafty.trigger("NewEntity", {
id: id
});
return craft;
},
/**@
* #Crafty.c
* @category Core
* @sign public void Crafty.c(String name, Object component)
* @param name - Name of the component
* @param component - Object with the components properties and methods
* Creates a component where the first argument is the ID and the second
* is the object that will be inherited by entities.
*
* There is a convention for writing components.
*
* - Properties or methods that start with an underscore are considered private.
* - A method called `init` will automatically be called as soon as the
* component is added to an entity.
* - A methid called `uninit` will be called when the component is removed from an entity.
* A sample use case for this is the native DOM component that removes its div element wehen removed from an entity.
* - A method with the same name as the component is considered to be a constructor
* and is generally used when you need to pass configuration data to the component on a per entity basis.
*
* @example
* ~~~
* Crafty.c("Annoying", {
* _message: "HiHi",
* init: function() {
* this.bind("EnterFrame", function() { alert(this.message); });
* },
* annoying: function(message) { this.message = message; }
* });
*
* Crafty.e("Annoying").annoying("I'm an orange...");
* ~~~
*
* @see Crafty.e
*/
c: function (compName, component) {
components[compName] = component;
},
/**@
* #Crafty.trigger
* @category Core, Events
* @sign public void Crafty.trigger(String eventName, * data)
* @param eventName - Name of the event to trigger
* @param data - Arbitrary data to pass into the callback as an argument
*
* This method will trigger every single callback attached to the event name. This means
* every global event and every entity that has a callback.
*
* @see Crafty.bind
*/
trigger: function (event, data) {
var hdl = handlers[event], h, i, l;
//loop over every object bound
for (h in hdl) {
if (!hdl.hasOwnProperty(h)) continue;
//loop over every handler within object
for (i = 0, l = hdl[h].length; i < l; i++) {
if (hdl[h] && hdl[h][i]) {
//if an entity, call with that context
if (entities[h]) {
hdl[h][i].call(Crafty(+h), data);
} else { //else call with Crafty context
hdl[h][i].call(Crafty, data);
}
}
}
}
},
/**@
* #Crafty.bind
* @category Core, Events
* @sign public Number bind(String eventName, Function callback)
* @param eventName - Name of the event to bind to
* @param callback - Method to execute upon event triggered
* @returns ID of the current callback used to unbind
*
* Binds to a global event. Method will be executed when `Crafty.trigger` is used
* with the event name.
*
* @see Crafty.trigger, Crafty.unbind
*/
bind: function (event, callback) {
if (!handlers[event]) handlers[event] = {};
var hdl = handlers[event];
if (!hdl.global) hdl.global = [];
return hdl.global.push(callback) - 1;
},
/**@
* #Crafty.unbind
* @category Core, Events
* @sign public Boolean Crafty.unbind(String eventName, Function callback)
* @param eventName - Name of the event to unbind
* @param callback - Function to unbind
* @sign public Boolean Crafty.unbind(String eventName, Number callbackID)
* @param callbackID - ID of the callback
* @returns True or false depending on if a callback was unbound
* Unbind any event from any entity or global event.
*/
unbind: function (event, callback) {
var hdl = handlers[event], h, i, l;
//loop over every object bound
for (h in hdl) {
if (!hdl.hasOwnProperty(h)) continue;
//if passed the ID
if (typeof callback === "number") {
delete hdl[h][callback];
return true;
}
//loop over every handler within object
for (i = 0, l = hdl[h].length; i < l; i++) {
if (hdl[h][i] === callback) {
delete hdl[h][i];
return true;
}
}
}
return false;
},
/**@
* #Crafty.frame
* @category Core
* @sign public Number Crafty.frame(void)
* Returns the current frame number
*/
frame: function () {
return frame;
},
components: function () {
return components;
},
isComp: function (comp) {
return comp in components;
},
debug: function () {
return entities;
},
/**@
* #Crafty.settings
* @category Core
* Modify the inner workings of Crafty through the settings.
*/
settings: (function () {
var states = {},
callbacks = {};
return {
/**@
* #Crafty.settings.register
* @comp Crafty.settings
* @sign public void Crafty.settings.register(String settingName, Function callback)
* @param settingName - Name of the setting
* @param callback - Function to execute when use modifies setting
*
* Use this to register custom settings. Callback will be executed when `Crafty.settings.modify` is used.
*
* @see Crafty.settings.modify
*/
register: function (setting, callback) {
callbacks[setting] = callback;
},
/**@
* #Crafty.settings.modify
* @comp Crafty.settings
* @sign public void Crafty.settings.modify(String settingName, * value)
* @param settingName - Name of the setting
* @param value - Value to set the setting to
*
* Modify settings through this method.
*
* @see Crafty.settings.register, Crafty.settings.get
*/
modify: function (setting, value) {
if (!callbacks[setting]) return;
callbacks[setting].call(states[setting], value);
states[setting] = value;
},
/**@
* #Crafty.settings.get
* @comp Crafty.settings
* @sign public * Crafty.settings.get(String settingName)
* @param settingName - Name of the setting
* @returns Current value of the setting
*
* Returns the current value of the setting.
*
* @see Crafty.settings.register, Crafty.settings.get
*/
get: function (setting) {
return states[setting];
}
};
})(),
clone: clone
});
/**
* Return a unique ID
*/
function UID() {
var id = GUID++;
//if GUID is not unique
if (id in entities) {
return UID(); //recurse until it is unique
}
return id;
}
/**@
* #Crafty.clone
* @category Core
* @sign public Object .clone(Object obj)
* @param obj - an object
*
* Deep copy (a.k.a clone) of an object.
*/
function clone(obj) {
if (obj === null || typeof(obj) != 'object')
return obj;
var temp = obj.constructor(); // changed
for (var key in obj)
temp[key] = clone(obj[key]);
return temp;
}
Crafty.bind("Load", function () {
if (!Crafty.support.setter && Crafty.support.defineProperty) {
noSetter = [];
Crafty.bind("EnterFrame", function () {
var i = 0, l = noSetter.length, current;
for (; i < l; ++i) {
current = noSetter[i];
if (current.obj[current.prop] !== current.obj['_' + current.prop]) {
current.fn.call(current.obj, current.obj[current.prop]);
}
}
});
}
});
//make Crafty global
window.Crafty = Crafty;
})(window);
//wrap around components
(function(Crafty, window, document) {
/**
* Spatial HashMap for broad phase collision
*
* @author Louis Stowasser
*/
(function (parent) {
/**@
* #Crafty.HashMap.constructor
* @comp Crafty.HashMap
* @sign public void Crafty.HashMap([cellsize])
* @param cellsize - the cell size. If omitted, `cellsize` is 64.
*
* Set `cellsize`.
* And create `this.map`.
*/
var cellsize,
HashMap = function (cell) {
cellsize = cell || 64;
this.map = {};
},
SPACE = " ";
HashMap.prototype = {
/**@
* #Crafty.map.insert
* @comp Crafty.map
* @sign public Object Crafty.map.insert(Object obj)
* @param obj - An entity to be inserted.
*
* `obj` is instered in '.map' of the corresponding broad phase cells. An object of the following fields is returned.
* ~~~
* - the object that keep track of cells (keys)
* - `obj`
* - the HashMap object
* ~~~
*/
insert: function (obj) {
var keys = HashMap.key(obj),
entry = new Entry(keys, obj, this),
i = 0,
j,
hash;
//insert into all x buckets
for (i = keys.x1; i <= keys.x2; i++) {
//insert into all y buckets
for (j = keys.y1; j <= keys.y2; j++) {
hash = i + SPACE + j;
if (!this.map[hash]) this.map[hash] = [];
this.map[hash].push(obj);
}
}
return entry;
},
/**@
* #Crafty.map.search
* @comp Crafty.map
* @sign public Object Crafty.map.search(Object rect[, Boolean filter])
* @param rect - the rectangular region to search for entities.
* @param filter - Default value is true. Otherwise, must be false.
*
* - If `filter` is `false`, just search for all the entries in the give `rect` region by broad phase collision. Entity may be returned duplicated.
* - If `filter` is `true`, filter the above results by checking that they actually overlap `rect`.
* The easier usage is with `filter`=`true`. For performance reason, you may use `filter`=`false`, and filter the result youself. See examples in drawing.js and collision.js
*/
search: function (rect, filter) {
var keys = HashMap.key(rect),
i, j,l,
hash,
results = [];
if (filter === undefined) filter = true; //default filter to true
//search in all x buckets
for (i = keys.x1; i <= keys.x2; i++) {
//insert into all y buckets
for (j = keys.y1; j <= keys.y2; j++) {
hash = i + SPACE + j;
if (this.map[hash]) {
results = results.concat(this.map[hash]);
}
}
}
if (filter) {
var obj, id, finalresult = [], found = {};
//add unique elements to lookup table with the entity ID as unique key
for (i = 0, l = results.length; i < l; i++) {
obj = results[i];
if (!obj) continue; //skip if deleted
id = obj[0]; //unique ID
//check if not added to hash and that actually intersects
if (!found[id] && obj.x < rect._x + rect._w && obj._x + obj._w > rect._x &&
obj.y < rect._y + rect._h && obj._h + obj._y > rect._y)
found[id] = results[i];
}
//loop over lookup table and copy to final array
for (obj in found) finalresult.push(found[obj]);
return finalresult;
} else {
return results;
}
},
/**@
* #Crafty.map.remove
* @comp Crafty.map
* @sign public void Crafty.map.remove([Object keys, ]Object obj)
* @param keys - key region. If omitted, it will be derived from obj by `Crafty.HashMap.key`.
* @param obj - need more document.
*
* Remove an entity in a broad phase map.
* - The second form is only used in Crafty.HashMap to save time for computing keys again, where keys were computed previously from obj. End users should not call this form directly.
*
* @example
* ~~~
* Crafty.map.remove(e);
* ~~~
*/
remove: function (keys, obj) {
var i = 0, j, hash;
if (arguments.length == 1) {
obj = keys;
keys = HashMap.key(obj);
}
//search in all x buckets
for (i = keys.x1; i <= keys.x2; i++) {
//insert into all y buckets
for (j = keys.y1; j <= keys.y2; j++) {
hash = i + SPACE + j;
if (this.map[hash]) {
var cell = this.map[hash],
m,
n = cell.length;
//loop over objs in cell and delete
for (m = 0; m < n; m++)
if (cell[m] && cell[m][0] === obj[0])
cell.splice(m, 1);
}
}
}
},
/**@
* #Crafty.map.boundaries
* @comp Crafty.map
* @sign public Object Crafty.map.boundaries()
*
* The return `Object` is of the following format.
* ~~~
* {
* min: {
* x: val_x,
* y: val_y
* },
* max: {
* x: val_x,
* y: val_y
* }
* }
* ~~~
*/
boundaries: function () {
var k, ent,
hash = {
max: {
x: -Infinity,
y: -Infinity
},
min: {
x: Infinity,
y: Infinity
}
},
coords = {
max: {
x: -Infinity,
y: -Infinity
},
min: {
x: Infinity,
y: Infinity
}
};
//Using broad phase hash to speed up the computation of boundaries.
for (var h in this.map) {
if (!this.map[h].length) continue;
//broad phase coordinate
var map_coord = h.split(SPACE),
i=map_coord[0],
j=map_coord[0];
if (i >= hash.max.x) {
hash.max.x = i;
for (k in this.map[h]) {
ent = this.map[h][k];
//make sure that this is a Crafty entity
if (typeof ent == 'object' && 'requires' in ent) {
coords.max.x = Math.max(coords.max.x, ent.x + ent.w);
}
}
}
if (i <= hash.min.x) {
hash.min.x = i;
for (k in this.map[h]) {
ent = this.map[h][k];
if (typeof ent == 'object' && 'requires' in ent) {
coords.min.x = Math.min(coords.min.x, ent.x);
}
}
}
if (j >= hash.max.y) {
hash.max.y = j;
for (k in this.map[h]) {
ent = this.map[h][k];
if (typeof ent == 'object' && 'requires' in ent) {
coords.max.y = Math.max(coords.max.y, ent.y + ent.h);
}
}
}
if (j <= hash.min.y) {
hash.min.y = j;
for (k in this.map[h]) {
ent = this.map[h][k];
if (typeof ent == 'object' && 'requires' in ent) {
coords.min.y = Math.min(coords.min.y, ent.y);
}
}
}
}
return coords;
}
};
/**@
* #Crafty.HashMap
* @category 2D
* Broad-phase collision detection engine. See background information at
*
* ~~~
* - [N Tutorial B - Broad-Phase Collision](http://www.metanetsoftware.com/technique/tutorialB.html)
* - [Broad-Phase Collision Detection with CUDA](http.developer.nvidia.com/GPUGems3/gpugems3_ch32.html)
* ~~~
* @see Crafty.map
*/
/**@
* #Crafty.HashMap.key
* @comp Crafty.HashMap
* @sign public Object Crafty.HashMap.key(Object obj)
* @param obj - an Object that has .mbr() or _x, _y, _w and _h.
* Get the rectangular region (in terms of the grid, with grid size `cellsize`), where the object may fall in. This region is determined by the object's bounding box.
* The `cellsize` is 64 by default.
*
* @see Crafty.HashMap.constructor
*/
HashMap.key = function (obj) {
if (obj.hasOwnProperty('mbr')) {
obj = obj.mbr();
}
var x1 = ~~(obj._x / cellsize),
y1 = ~~(obj._y / cellsize),
x2 = ~~((obj._w + obj._x) / cellsize),
y2 = ~~((obj._h + obj._y) / cellsize);
return {
x1: x1,
y1: y1,
x2: x2,
y2: y2
};
};
HashMap.hash = function (keys) {
return keys.x1 + SPACE + keys.y1 + SPACE + keys.x2 + SPACE + keys.y2;
};
function Entry(keys, obj, map) {
this.keys = keys;
this.map = map;
this.obj = obj;
}
Entry.prototype = {
update: function (rect) {
//check if buckets change
if (HashMap.hash(HashMap.key(rect)) != HashMap.hash(this.keys)) {
this.map.remove(this.keys, this.obj);
var e = this.map.insert(this.obj);
this.keys = e.keys;
}
}
};
parent.HashMap = HashMap;
})(Crafty);
/**@
* #Crafty.map
* @category 2D
* Functions related with quering entities.
* @see Crafty.HashMap
*/
Crafty.map = new Crafty.HashMap();
var M = Math,
Mc = M.cos,
Ms = M.sin,
PI = M.PI,
DEG_TO_RAD = PI / 180;
/**@
* #2D
* @category 2D
* Component for any entity that has a position on the stage.
* @trigger Move - when the entity has moved - { _x:Number, _y:Number, _w:Number, _h:Number } - Old position
* @trigger Change - when the entity has moved - { _x:Number, _y:Number, _w:Number, _h:Number } - Old position
* @trigger Rotate - when the entity is rotated - { cos:Number, sin:Number, deg:Number, rad:Number, o: {x:Number, y:Number}, matrix: {M11, M12, M21, M22} }
*/
Crafty.c("2D", {
/**@
* #.x
* @comp 2D
* The `x` position on the stage. When modified, will automatically be redrawn.
* Is actually a getter/setter so when using this value for calculations and not modifying it,
* use the `._x` property.
* @see ._attr
*/
_x: 0,
/**@
* #.y
* @comp 2D
* The `y` position on the stage. When modified, will automatically be redrawn.
* Is actually a getter/setter so when using this value for calculations and not modifying it,
* use the `._y` property.
* @see ._attr
*/
_y: 0,
/**@
* #.w
* @comp 2D
* The width of the entity. When modified, will automatically be redrawn.
* Is actually a getter/setter so when using this value for calculations and not modifying it,
* use the `._w` property.
*
* Changing this value is not recommended as canvas has terrible resize quality and DOM will just clip the image.
* @see ._attr
*/
_w: 0,
/**@
* #.h
* @comp 2D
* The height of the entity. When modified, will automatically be redrawn.
* Is actually a getter/setter so when using this value for calculations and not modifying it,
* use the `._h` property.
*
* Changing this value is not recommended as canvas has terrible resize quality and DOM will just clip the image.
* @see ._attr
*/
_h: 0,
/**@
* #.z
* @comp 2D
* The `z` index on the stage. When modified, will automatically be redrawn.
* Is actually a getter/setter so when using this value for calculations and not modifying it,
* use the `._z` property.
*
* A higher `z` value will be closer to the front of the stage. A smaller `z` value will be closer to the back.
* A global Z index is produced based on its `z` value as well as the GID (which entity was created first).
* Therefore entities will naturally maintain order depending on when it was created if same z value.
* @see ._attr
*/
_z: 0,
/**@
* #.rotation
* @comp 2D
* Set the rotation of your entity. Rotation takes degrees in a clockwise direction.
* It is important to note there is no limit on the rotation value. Setting a rotation
* mod 360 will give the same rotation without reaching huge numbers.
* @see ._attr
*/
_rotation: 0,
/**@
* #.alpha
* @comp 2D
* Transparency of an entity. Must be a decimal value between 0.0 being fully transparent to 1.0 being fully opaque.
*/
_alpha: 1.0,
/**@
* #.visible
* @comp 2D
* If the entity is visible or not. Accepts a true or false value.
* Can be used for optimization by setting an entities visibility to false when not needed to be drawn.
*
* The entity will still exist and can be collided with but just won't be drawn.
* @see Crafty.DrawManager.draw, Crafty.DrawManager.drawAll
*/
_visible: true,
/**@
* #._globalZ
* @comp 2D
* When two entities overlap, the one with the larger `_globalZ` will be on top of the other.
* @see Crafty.DrawManager.draw, Crafty.DrawManager.drawAll
*/
_globalZ: null,
_origin: null,
_mbr: null,
_entry: null,
_children: null,
_parent: null,
_changed: false,
_defineGetterSetter_setter: function() {
//create getters and setters using __defineSetter__ and __defineGetter__
this.__defineSetter__('x', function (v) {
this._attr('_x', v);
});
this.__defineSetter__('y', function (v) {
this._attr('_y', v);
});
this.__defineSetter__('w', function (v) {
this._attr('_w', v);
});
this.__defineSetter__('h', function (v) {
this._attr('_h', v);
});
this.__defineSetter__('z', function (v) {
this._attr('_z', v);
});
this.__defineSetter__('rotation', function (v) {
this._attr('_rotation', v);
});
this.__defineSetter__('alpha', function (v) {
this._attr('_alpha', v);
});
this.__defineSetter__('visible', function (v) {
this._attr('_visible', v);
});
this.__defineGetter__('x', function () {
return this._x;
});
this.__defineGetter__('y', function () {
return this._y;
});
this.__defineGetter__('w', function () {
return this._w;
});
this.__defineGetter__('h', function () {
return this._h;
});
this.__defineGetter__('z', function () {
return this._z;
});
this.__defineGetter__('rotation', function () {
return this._rotation;
});
this.__defineGetter__('alpha', function () {
return this._alpha;
});
this.__defineGetter__('visible', function () {
return this._visible;
});
this.__defineGetter__('parent', function () {
return this._parent;
});
this.__defineGetter__('numChildren', function () {
return this._children.length;
});
},
_defineGetterSetter_defineProperty: function() {
Object.defineProperty(this, 'x', {
set: function (v) {
this._attr('_x', v);
}
,
get: function () {
return this._x;
}
,
configurable: true
});
Object.defineProperty(this, 'y', {
set: function (v) {
this._attr('_y', v);
}
,
get: function () {
return this._y;
}
,
configurable: true
});
Object.defineProperty(this, 'w', {
set: function (v) {
this._attr('_w', v);
}
,
get: function () {
return this._w;
}
,
configurable: true
});
Object.defineProperty(this, 'h', {
set: function (v) {
this._attr('_h', v);
}
,
get: function () {
return this._h;
}
,
configurable: true
});
Object.defineProperty(this, 'z', {
set: function (v) {
this._attr('_z', v);
}
,
get: function () {
return this._z;
}
,
configurable: true
});
Object.defineProperty(this, 'rotation', {
set: function (v) {
this._attr('_rotation', v);
}
,
get: function () {
return this._rotation;
}
,
configurable: true
});
Object.defineProperty(this, 'alpha', {
set: function (v) {
this._attr('_alpha', v);
}
,
get: function () {
return this._alpha;
}
,
configurable: true
});
Object.defineProperty(this, 'visible', {
set: function (v) {
this._attr('_visible', v);
}
,
get: function () {
return this._visible;
}
,
configurable: true
});
},
_defineGetterSetter_fallback: function() {
//set the public properties to the current private properties
this.x = this._x;
this.y = this._y;
this.w = this._w;
this.h = this._h;
this.z = this._z;
this.rotation = this._rotation;
this.alpha = this._alpha;
this.visible = this._visible;
//on every frame check for a difference in any property
this.bind("EnterFrame", function () {
//if there are differences between the public and private properties
if (this.x !== this._x || this.y !== this._y ||
this.w !== this._w || this.h !== this._h ||
this.z !== this._z || this.rotation !== this._rotation ||
this.alpha !== this._alpha || this.visible !== this._visible) {
//save the old positions
var old = this.mbr() || this.pos();
//if rotation has changed, use the private rotate method
if (this.rotation !== this._rotation) {
this._rotate(this.rotation);
} else {
//update the MBR
var mbr = this._mbr, moved = false;
// If the browser doesn't have getters or setters,
// {x, y, w, h, z} and {_x, _y, _w, _h, _z} may be out of synce,
// in which case t checks if they are different on tick and executes the Change event.
if (mbr) { //check each value to see which has changed
if (this.x !== this._x) {
mbr._x -= this.x - this._x;
moved = true;
}
else if (this.y !== this._y) {
mbr._y -= this.y - this._y;
moved = true;
}
else if (this.w !== this._w) {
mbr._w -= this.w - this._w;
moved = true;
}
else if (this.h !== this._h) {
mbr._h -= this.h - this._h;
moved = true;
}
else if (this.z !== this._z) {
mbr._z -= this.z - this._z;
moved = true;
}
}
//if the moved flag is true, trigger a move
if (moved) this.trigger("Move", old);
}
//set the public properties to the private properties
this._x = this.x;
this._y = this.y;
this._w = this.w;
this._h = this.h;
this._z = this.z;
this._rotation = this.rotation;
this._alpha = this.alpha;
this._visible = this.visible;
//trigger the changes
this.trigger("Change", old);
//without this entities weren't added correctly to Crafty.map.map in IE8.
//not entirely sure this is the best way to fix it though
this.trigger("Move", old);
}
});
},
init: function() {
this._globalZ = this[0];
this._origin = {
x: 0,
y: 0
};
this._children = [];
if(Crafty.support.setter) {
this._defineGetterSetter_setter();
} else if (Crafty.support.defineProperty) {
//IE9 supports Object.defineProperty
this._defineGetterSetter_defineProperty();
} else {
/*
If no setters and getters are supported (e.g. IE8) supports,
check on every frame for a difference between this._(x|y|w|h|z...)
and this.(x|y|w|h|z) and update accordingly.
*/
this._defineGetterSetter_fallback();
}
//insert self into the HashMap
this._entry = Crafty.map.insert(this);
//when object changes, update HashMap
this.bind("Move", function (e) {
var area = this._mbr || this;
this._entry.update(area);
this._cascade(e);
});
this.bind("Rotate", function (e) {
var old = this._mbr || this;
this._entry.update(old);
this._cascade(e);
});
//when object is removed, remove from HashMap and destroy attached children
this.bind("Remove", function () {
if (this._children) {
for (var i = 0; i < this._children.length; i++) {
if (this._children[i].destroy) {
this._children[i].destroy();
}
}
this._children = [];
}
Crafty.map.remove(this);
this.detach();
});
},
/**
* Calculates the MBR when rotated with an origin point
*/
_rotate: function (v) {
var theta = -1 * (v % 360), //angle always between 0 and 359
rad = theta * DEG_TO_RAD,
ct = Math.cos(rad), //cache the sin and cosine of theta
st = Math.sin(rad),
o = {
x: this._origin.x + this._x,
y: this._origin.y + this._y
};
//if the angle is 0 and is currently 0, skip
if (!theta) {
this._mbr = null;
if (!this._rotation % 360) return;
}
var x0 = o.x + (this._x - o.x) * ct + (this._y - o.y) * st,
y0 = o.y - (this._x - o.x) * st + (this._y - o.y) * ct,
x1 = o.x + (this._x + this._w - o.x) * ct + (this._y - o.y) * st,
y1 = o.y - (this._x + this._w - o.x) * st + (this._y - o.y) * ct,
x2 = o.x + (this._x + this._w - o.x) * ct + (this._y + this._h - o.y) * st,
y2 = o.y - (this._x + this._w - o.x) * st + (this._y + this._h - o.y) * ct,
x3 = o.x + (this._x - o.x) * ct + (this._y + this._h - o.y) * st,
y3 = o.y - (this._x - o.x) * st + (this._y + this._h - o.y) * ct,
minx = Math.floor(Math.min(x0, x1, x2, x3)),
miny = Math.floor(Math.min(y0, y1, y2, y3)),
maxx = Math.ceil(Math.max(x0, x1, x2, x3)),
maxy = Math.ceil(Math.max(y0, y1, y2, y3));
this._mbr = {
_x: minx,
_y: miny,
_w: maxx - minx,
_h: maxy - miny
};
//trigger rotation event
var difference = this._rotation - v,
drad = difference * DEG_TO_RAD;
this.trigger("Rotate", {
cos: Math.cos(drad),
sin: Math.sin(drad),
deg: difference,
rad: drad,
o: {
x: o.x,
y: o.y
},
matrix: {
M11: ct,
M12: st,
M21: -st,
M22: ct
}
});
},
/**@
* #.area
* @comp 2D
* @sign public Number .area(void)
* Calculates the area of the entity
*/
area: function () {
return this._w * this._h;
},
/**@
* #.intersect
* @comp 2D
* @sign public Boolean .intersect(Number x, Number y, Number w, Number h)
* @param x - X position of the rect
* @param y - Y position of the rect
* @param w - Width of the rect
* @param h - Height of the rect
* @sign public Boolean .intersect(Object rect)
* @param rect - An object that must have the `x, y, w, h` values as properties
* Determines if this entity intersects a rectangle.
*/
intersect: function (x, y, w, h) {
var rect, obj = this._mbr || this;
if (typeof x === "object") {
rect = x;
} else {
rect = {
x: x,
y: y,
w: w,
h: h
};
}
return obj._x < rect.x + rect.w && obj._x + obj._w > rect.x &&
obj._y < rect.y + rect.h && obj._h + obj._y > rect.y;
},
/**@
* #.within
* @comp 2D
* @sign public Boolean .within(Number x, Number y, Number w, Number h)
* @param x - X position of the rect
* @param y - Y position of the rect
* @param w - Width of the rect
* @param h - Height of the rect
* @sign public Boolean .within(Object rect)
* @param rect - An object that must have the `x, y, w, h` values as properties
* Determines if this current entity is within another rectangle.
*/
within: function (x, y, w, h) {
var rect;
if (typeof x === "object") {
rect = x;
} else {
rect = {
x: x,
y: y,
w: w,
h: h
};
}
return rect.x <= this.x && rect.x + rect.w >= this.x + this.w &&
rect.y <= this.y && rect.y + rect.h >= this.y + this.h;
},
/**@
* #.contains
* @comp 2D
* @sign public Boolean .contains(Number x, Number y, Number w, Number h)
* @param x - X position of the rect
* @param y - Y position of the rect
* @param w - Width of the rect
* @param h - Height of the rect
* @sign public Boolean .contains(Object rect)
* @param rect - An object that must have the `x, y, w, h` values as properties
* Determines if the rectangle is within the current entity.
*/
contains: function (x, y, w, h) {
var rect;
if (typeof x === "object") {
rect = x;
} else {
rect = {
x: x,
y: y,
w: w,
h: h
};
}
return rect.x >= this.x && rect.x + rect.w <= this.x + this.w &&
rect.y >= this.y && rect.y + rect.h <= this.y + this.h;
},
/**@
* #.pos
* @comp 2D
* @sign public Object .pos(void)
* Returns the x, y, w, h properties as a rect object
* (a rect object is just an object with the keys _x, _y, _w, _h).
*
* The keys have an underscore prefix. This is due to the x, y, w, h
* properties being merely setters and getters that wrap the properties with an underscore (_x, _y, _w, _h).
*/
pos: function () {
return {
_x: (this._x),
_y: (this._y),
_w: (this._w),
_h: (this._h)
};
},
/**@
* #.mbr
* @comp 2D
* @sign public Object .mbr()
* Returns the minimum bounding rectangle. If there is no rotation
* on the entity it will return the rect.
*/
mbr: function () {
if (!this._mbr) return this.pos();
return {
_x: (this._mbr._x),
_y: (this._mbr._y),
_w: (this._mbr._w),
_h: (this._mbr._h)
};
},
/**@
* #.isAt
* @comp 2D
* @sign public Boolean .isAt(Number x, Number y)
* @param x - X position of the point
* @param y - Y position of the point
* Determines whether a point is contained by the entity. Unlike other methods,
* an object can't be passed. The arguments require the x and y value
*/
isAt: function (x, y) {
if (this.mapArea) {
return this.mapArea.containsPoint(x, y);
} else if (this.map) {
return this.map.containsPoint(x, y);
}
return this.x <= x && this.x + this.w >= x &&
this.y <= y && this.y + this.h >= y;
},
/**@
* #.move
* @comp 2D
* @sign public this .move(String dir, Number by)
* @param dir - Direction to move (n,s,e,w,ne,nw,se,sw)
* @param by - Amount to move in the specified direction
* Quick method to move the entity in a direction (n, s, e, w, ne, nw, se, sw) by an amount of pixels.
*/
move: function (dir, by) {
if (dir.charAt(0) === 'n') this.y -= by;
if (dir.charAt(0) === 's') this.y += by;
if (dir === 'e' || dir.charAt(1) === 'e') this.x += by;
if (dir === 'w' || dir.charAt(1) === 'w') this.x -= by;
return this;
},
/**@
* #.shift
* @comp 2D
* @sign public this .shift(Number x, Number y, Number w, Number h)
* @param x - Amount to move X
* @param y - Amount to move Y
* @param w - Amount to widen
* @param h - Amount to increase height
* Shift or move the entity by an amount. Use negative values
* for an opposite direction.
*/
shift: function (x, y, w, h) {
if (x) this.x += x;
if (y) this.y += y;
if (w) this.w += w;
if (h) this.h += h;
return this;
},
/**@
* #._cascade
* @comp 2D
* @sign public void ._cascade(e)
* @param e - Amount to move X
* Shift move or rotate the entity by an amount. Use negative values
* for an opposite direction.
*/
_cascade: function (e) {
if (!e) return; //no change in position
var i = 0, children = this._children, l = children.length, obj;
//rotation
if (e.cos) {
for (; i < l; ++i) {
obj = children[i];
if ('rotate' in obj) obj.rotate(e);
}
} else {
//use MBR or current
var rect = this._mbr || this,
dx = rect._x - e._x,
dy = rect._y - e._y,
dw = rect._w - e._w,
dh = rect._h - e._h;
for (; i < l; ++i) {
obj = children[i];
obj.shift(dx, dy, dw, dh);
}
}
},
/**@
* #.attach
* @comp 2D
* @sign public this .attach(Entity obj[, .., Entity objN])
* @param obj - Entity(s) to attach
* Attaches an entities position and rotation to current entity. When the current entity moves,
* the attached entity will move by the same amount.
*
* As many objects as wanted can be attached and a hierarchy of objects is possible by attaching.
*/
attach: function () {
var i = 0, arg = arguments, l = arguments.length, obj;
for (; i < l; ++i) {
obj = arg[i];
if (obj._parent) {
obj._parent.detach(obj);
}
obj._parent = this;
this._children.push(obj);
}
return this;
},
/**@
* #.detach
* @comp 2D
* @sign public this .detach([Entity obj])
* @param obj - The entity to detach. Left blank will remove all attached entities
* Stop an entity from following the current entity. Passing no arguments will stop
* every entity attached.
*/
detach: function (obj) {
//if nothing passed, remove all attached objects
if (!obj) {
for (var i = 0; i < this._children.length; i++) {
this._children[i]._parent = null;
}
this._children = [];
return this;
}
//if obj passed, find the handler and unbind
for (var i = 0; i < this._children.length; i++) {
if (this._children[i] == obj) {
this._children.splice(i, 1);
}
}
obj._parent = null;
return this;
},
/**@
* #.origin
* @comp 2D
* @sign public this .origin(Number x, Number y)
* @param x - Pixel value of origin offset on the X axis
* @param y - Pixel value of origin offset on the Y axis
* @sign public this .origin(String offset)
* @param offset - Combination of center, top, bottom, middle, left and right
* Set the origin point of an entity for it to rotate around.
*
* @example
* ~~~
* this.origin("top left")
* this.origin("center")
* this.origin("bottom right")
* this.origin("middle right")
* ~~~
*
* @see .rotation
*/
origin: function (x, y) {
//text based origin
if (typeof x === "string") {
if (x === "centre" || x === "center" || x.indexOf(' ') === -1) {
x = this._w / 2;
y = this._h / 2;
} else {
var cmd = x.split(' ');
if (cmd[0] === "top") y = 0;
else if (cmd[0] === "bottom") y = this._h;
else if (cmd[0] === "middle" || cmd[1] === "center" || cmd[1] === "centre") y = this._h / 2;
if (cmd[1] === "center" || cmd[1] === "centre" || cmd[1] === "middle") x = this._w / 2;
else if (cmd[1] === "left") x = 0;
else if (cmd[1] === "right") x = this._w;
}
}
this._origin.x = x;
this._origin.y = y;
return this;
},
/**@
* #.flip
* @comp 2D
* @trigger Change - when the entity has flipped
* @sign public this .flip(String dir)
* @param dir - Flip direction
*
* Flip entity on passed direction
*
* @example
* ~~~
* this.flip("X")
* ~~~
*/
flip: function (dir) {
dir = dir || "X";
if(!this["_flip" + dir]) {
this["_flip" + dir] = true;
this.trigger("Change");
}
},
/**@
* #.unflip
* @comp 2D
* @trigger Change - when the entity has unflipped
* @sign public this .unflip(String dir)
* @param dir - Unflip direction
*
* Unflip entity on passed direction (if it's flipped)
*
* @example
* ~~~
* this.unflip("X")
* ~~~
*/
unflip: function (dir) {
dir = dir || "X";
if(this["_flip" + dir]) {
this["_flip" + dir] = false;
this.trigger("Change");
}
},
/**
* Method for rotation rather than through a setter
*/
rotate: function (e) {
//assume event data origin
this._origin.x = e.o.x - this._x;
this._origin.y = e.o.y - this._y;
//modify through the setter method
this._attr('_rotation', e.theta);
},
/**@
* #._attr
* @comp 2D
* Setter method for all 2D properties including
* x, y, w, h, alpha, rotation and visible.
*/
_attr: function (name, value) {
//keep a reference of the old positions
var pos = this.pos(),
old = this.mbr() || pos;
//if rotation, use the rotate method
if (name === '_rotation') {
this._rotate(value);
this.trigger("Rotate");
//set the global Z and trigger reorder just incase
} else if (name === '_z') {
this._globalZ = parseInt(value + Crafty.zeroFill(this[0], 5), 10); //magic number 10e5 is the max num of entities
this.trigger("reorder");
//if the rect bounds change, update the MBR and trigger move
} else if (name == '_x' || name === '_y' || name === '_w' || name === '_h') {
var mbr = this._mbr;
if (mbr) {
mbr[name] -= this[name] - value;
}
this[name] = value;
this.trigger("Move", old);
}
//everything will assume the value
this[name] = value;
//trigger a change
this.trigger("Change", old);
}
});
Crafty.c("Physics", {
_gravity: 0.4,
_friction: 0.2,
_bounce: 0.5,
gravity: function (gravity) {
this._gravity = gravity;
}
});
/**@
* #Gravity
* @category 2D
* Adds gravitational pull to the entity.
*/
Crafty.c("Gravity", {
_gravityConst: 0.2,
_gy: 0,
_falling: true,
_anti: null,
init: function () {
this.requires("2D");
},
/**@
* #.gravity
* @comp Gravity
* @sign public this .gravity([comp])
* @param comp - The name of a component that will stop this entity from falling
*
* Enable gravity for this entity no matter whether comp parameter is not specified,
* If comp parameter is specified all entities with that component will stop this entity from falling.
* For a player entity in a platform game this would be a component that is added to all entities
* that the player should be able to walk on.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Color, Gravity")
* .color("red")
* .attr({ w: 100, h: 100 })
* .gravity("platform")
* ~~~
*/
gravity: function (comp) {
if (comp) this._anti = comp;
this.bind("EnterFrame", this._enterFrame);
return this;
},
/**@
* #.gravityConst
* @comp Gravity
* @sign public this .gravityConst(g)
* @param g - gravitational constant
*
* Set the gravitational constant to g. The default is .2. The greater g, the faster the object falls.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Color, Gravity")
* .color("red")
* .attr({ w: 100, h: 100 })
* .gravity("platform")
* .gravityConst(2)
* ~~~
*/
gravityConst: function(g) {
this._gravityConst=g;
return this;
},
_enterFrame: function () {
if (this._falling) {
//if falling, move the players Y
this._gy += this._gravityConst;
this.y += this._gy;
} else {
this._gy = 0; //reset change in y
}
var obj, hit = false, pos = this.pos(),
q, i = 0, l;
//Increase by 1 to make sure map.search() finds the floor
pos._y++;
//map.search wants _x and intersect wants x...
pos.x = pos._x;
pos.y = pos._y;
pos.w = pos._w;
pos.h = pos._h;
q = Crafty.map.search(pos);
l = q.length;
for (; i < l; ++i) {
obj = q[i];
//check for an intersection directly below the player
if (obj !== this && obj.has(this._anti) && obj.intersect(pos)) {
hit = obj;
break;
}
}
if (hit) { //stop falling if found
if (this._falling) this.stopFalling(hit);
} else {
this._falling = true; //keep falling otherwise
}
},
stopFalling: function (e) {
if (e) this.y = e._y - this._h; //move object
//this._gy = -1 * this._bounce;
this._falling = false;
if (this._up) this._up = false;
this.trigger("hit");
},
/**@
* #.antigravity
* @comp Gravity
* @sign public this .antigravity()
* Disable gravity for this component. It can be reenabled by calling .gravity()
*/
antigravity: function () {
this.unbind("EnterFrame", this._enterFrame);
}
});
/**@
* #Crafty.polygon
* @category 2D
*
* Polygon object used for hitboxes and click maps. Must pass an Array for each point as an
* argument where index 0 is the x position and index 1 is the y position.
*
* For example one point of a polygon will look like this: `[0,5]` where the `x` is `0` and the `y` is `5`.
*
* Can pass an array of the points or simply put each point as an argument.
*
* When creating a polygon for an entity, each point should be offset or relative from the entities `x` and `y`
* (don't include the absolute values as it will automatically calculate this).
*
*
* @example
* ~~~
* new Crafty.polygon([50,0],[100,100],[0,100]);
* new Crafty.polygon([[50,0],[100,100],[0,100]]);
* ~~~
*/
Crafty.polygon = function (poly) {
if (arguments.length > 1) {
poly = Array.prototype.slice.call(arguments, 0);
}
this.points = poly;
};
Crafty.polygon.prototype = {
/**@
* #.containsPoint
* @comp Crafty.polygon
* @sign public Boolean .containsPoint(Number x, Number y)
* @param x - X position of the point
* @param y - Y position of the point
*
* Method is used to determine if a given point is contained by the polygon.
*
* @example
* ~~~
* var poly = new Crafty.polygon([50,0],[100,100],[0,100]);
* poly.containsPoint(50, 50); //TRUE
* poly.containsPoint(0, 0); //FALSE
* ~~~
*/
containsPoint: function (x, y) {
var p = this.points, i, j, c = false;
for (i = 0, j = p.length - 1; i < p.length; j = i++) {
if (((p[i][1] > y) != (p[j][1] > y)) && (x < (p[j][0] - p[i][0]) * (y - p[i][1]) / (p[j][1] - p[i][1]) + p[i][0])) {
c = !c;
}
}
return c;
},
/**@
* #.shift
* @comp Crafty.polygon
* @sign public void .shift(Number x, Number y)
* @param x - Amount to shift the `x` axis
* @param y - Amount to shift the `y` axis
*
* Shifts every single point in the polygon by the specified amount.
*
* @example
* ~~~
* var poly = new Crafty.polygon([50,0],[100,100],[0,100]);
* poly.shift(5,5);
* //[[55,5], [105,5], [5,105]];
* ~~~
*/
shift: function (x, y) {
var i = 0, l = this.points.length, current;
for (; i < l; i++) {
current = this.points[i];
current[0] += x;
current[1] += y;
}
},
rotate: function (e) {
var i = 0, l = this.points.length,
current, x, y;
for (; i < l; i++) {
current = this.points[i];
x = e.o.x + (current[0] - e.o.x) * e.cos + (current[1] - e.o.y) * e.sin;
y = e.o.y - (current[0] - e.o.x) * e.sin + (current[1] - e.o.y) * e.cos;
current[0] = x;
current[1] = y;
}
}
};
/**@
* #Crafty.circle
* @category 2D
* Circle object used for hitboxes and click maps. Must pass a `x`, a `y` and a `radius` value.
*
*@example
* ~~~
* var centerX = 5,
* centerY = 10,
* radius = 25;
*
* new Crafty.circle(centerX, centerY, radius);
* ~~~
*
* When creating a circle for an entity, each point should be offset or relative from the entities `x` and `y`
* (don't include the absolute values as it will automatically calculate this).
*/
Crafty.circle = function (x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
// Creates an octogon that aproximate the circle for backward compatibility.
this.points = [];
var theta;
for (var i = 0; i < 8; i++) {
theta = i * Math.PI / 4;
this.points[i] = [Math.sin(theta) * radius, Math.cos(theta) * radius];
}
};
Crafty.circle.prototype = {
/**@
* #.containsPoint
* @comp Crafty.circle
* @sign public Boolean .containsPoint(Number x, Number y)
* @param x - X position of the point
* @param y - Y position of the point
*
* Method is used to determine if a given point is contained by the circle.
*
* @example
* ~~~
* var circle = new Crafty.circle(0, 0, 10);
* circle.containsPoint(0, 0); //TRUE
* circle.containsPoint(50, 50); //FALSE
* ~~~
*/
containsPoint: function (x, y) {
var radius = this.radius,
sqrt = Math.sqrt,
deltaX = this.x - x,
deltaY = this.y - y;
return (deltaX * deltaX + deltaY * deltaY) < (radius * radius);
},
/**@
* #.shift
* @comp Crafty.circle
* @sign public void .shift(Number x, Number y)
* @param x - Amount to shift the `x` axis
* @param y - Amount to shift the `y` axis
*
* Shifts the circle by the specified amount.
*
* @example
* ~~~
* var poly = new Crafty.circle(0, 0, 10);
* circle.shift(5,5);
* //{x: 5, y: 5, radius: 10};
* ~~~
*/
shift: function (x, y) {
this.x += x;
this.y += y;
var i = 0, l = this.points.length, current;
for (; i < l; i++) {
current = this.points[i];
current[0] += x;
current[1] += y;
}
},
rotate: function () {
// We are a circle, we don't have to rotate :)
}
};
Crafty.matrix = function (m) {
this.mtx = m;
this.width = m[0].length;
this.height = m.length;
};
Crafty.matrix.prototype = {
x: function (other) {
if (this.width != other.height) {
return;
}
var result = [];
for (var i = 0; i < this.height; i++) {
result[i] = [];
for (var j = 0; j < other.width; j++) {
var sum = 0;
for (var k = 0; k < this.width; k++) {
sum += this.mtx[i][k] * other.mtx[k][j];
}
result[i][j] = sum;
}
}
return new Crafty.matrix(result);
},
e: function (row, col) {
//test if out of bounds
if (row < 1 || row > this.mtx.length || col < 1 || col > this.mtx[0].length) return null;
return this.mtx[row - 1][col - 1];
}
}
/**@
* #Collision
* @category 2D
* Component to detect collision between any two convex polygons.
*/
Crafty.c("Collision", {
/**@
* #.init
* @comp Collision
* Create a rectangle polygon based on the x, y, w, h dimensions.
*/
init: function () {
this.requires("2D");
var area = this._mbr || this;
var poly = new Crafty.polygon([0, 0], [area._w, 0], [area._w, area._h], [0, area._h]);
this.map = poly;
this.attach(this.map);
this.map.shift(area._x, area._y);
},
/**@
* #.collision
* @comp Collision
*
* @sign public this .collision([Crafty.polygon polygon])
* @param polygon - Crafty.polygon object that will act as the hit area
*
* @sign public this .collision(Array point1, .., Array pointN)
* @param point# - Array with an `x` and `y` position to generate a polygon
*
* Constructor takes a polygon or array of points to use as the hit area.
*
* The hit area (polygon) must be a convex shape and not concave
* for the collision detection to work.
*
* @example
* ~~~
* Crafty.e("2D, Collision").collision(
* new Crafty.polygon([50,0], [100,100], [0,100])
* );
*
* Crafty.e("2D, Collision").collision([50,0], [100,100], [0,100]);
* ~~~
*
* @see Crafty.polygon
*/
collision: function (poly) {
var area = this._mbr || this;
if (!poly) {
return this;
}
if (arguments.length > 1) {
//convert args to array to create polygon
var args = Array.prototype.slice.call(arguments, 0);
poly = new Crafty.polygon(args);
}
this.map = poly;
this.attach(this.map);
this.map.shift(area._x, area._y);
return this;
},
/**@
* #.hit
* @comp Collision
* @sign public Boolean/Array hit(String component)
* @param component - Check collision with entities that has this component
* @return `false` if no collision. If a collision is detected, returns an Array of objects that are colliding.
*
* Takes an argument for a component to test collision for. If a collision is found, an array of
* every object in collision along with the amount of overlap is passed.
*
* If no collision, will return false. The return collision data will be an Array of Objects with the
* type of collision used, the object collided and if the type used was SAT (a polygon was used as the hitbox) then an amount of overlap.\
* ~~~
* [{
* obj: [entity],
* type "MBR" or "SAT",
* overlap: [number]
* }]
* ~~~
* `MBR` is your standard axis aligned rectangle intersection (`.intersect` in the 2D component).
* `SAT` is collision between any convex polygon.
*
* @see .onHit, 2D
*/
hit: function (comp) {
var area = this._mbr || this,
results = Crafty.map.search(area, false),
i = 0, l = results.length,
dupes = {},
id, obj, oarea, key,
hasMap = ('map' in this && 'containsPoint' in this.map),
finalresult = [];
if (!l) {
return false;
}
for (; i < l; ++i) {
obj = results[i];
oarea = obj._mbr || obj; //use the mbr
if (!obj) continue;
id = obj[0];
//check if not added to hash and that actually intersects
if (!dupes[id] && this[0] !== id && obj.__c[comp] &&
oarea._x < area._x + area._w && oarea._x + oarea._w > area._x &&
oarea._y < area._y + area._h && oarea._h + oarea._y > area._y)
dupes[id] = obj;
}
for (key in dupes) {
obj = dupes[key];
if (hasMap && 'map' in obj) {
var SAT = this._SAT(this.map, obj.map);
SAT.obj = obj;
SAT.type = "SAT";
if (SAT) finalresult.push(SAT);
} else {
finalresult.push({
obj: obj,
type: "MBR"
});
}
}
if (!finalresult.length) {
return false;
}
return finalresult;
},
/**@
* #.onHit
* @comp Collision
* @sign public this .onHit(String component, Function hit[, Function noHit])
* @param component - Component to check collisions for
* @param hit - Callback method to execute when collided with component
* @param noHit - Callback method executed once as soon as collision stops
*
* Creates an enterframe event calling .hit() each time and if collision detected will invoke the callback.
*
* @see .hit
*/
onHit: function (comp, callback, callbackOff) {
var justHit = false;
this.bind("EnterFrame", function () {
var hitdata = this.hit(comp);
if (hitdata) {
justHit = true;
callback.call(this, hitdata);
} else if (justHit) {
if (typeof callbackOff == 'function') {
callbackOff.call(this);
}
justHit = false;
}
});
return this;
},
_SAT: function (poly1, poly2) {
var points1 = poly1.points,
points2 = poly2.points,
i = 0, l = points1.length,
j, k = points2.length,
normal = {
x: 0,
y: 0
},
length,
min1, min2,
max1, max2,
interval,
MTV = null,
MTV2 = null,
MN = null,
dot,
nextPoint,
currentPoint;
//loop through the edges of Polygon 1
for (; i < l; i++) {
nextPoint = points1[(i == l - 1 ? 0 : i + 1)];
currentPoint = points1[i];
//generate the normal for the current edge
normal.x = -(nextPoint[1] - currentPoint[1]);
normal.y = (nextPoint[0] - currentPoint[0]);
//normalize the vector
length = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
normal.x /= length;
normal.y /= length;
//default min max
min1 = min2 = -1;
max1 = max2 = -1;
//project all vertices from poly1 onto axis
for (j = 0; j < l; ++j) {
dot = points1[j][0] * normal.x + points1[j][1] * normal.y;
if (dot > max1 || max1 === -1) max1 = dot;
if (dot < min1 || min1 === -1) min1 = dot;
}
//project all vertices from poly2 onto axis
for (j = 0; j < k; ++j) {
dot = points2[j][0] * normal.x + points2[j][1] * normal.y;
if (dot > max2 || max2 === -1) max2 = dot;
if (dot < min2 || min2 === -1) min2 = dot;
}
//calculate the minimum translation vector should be negative
if (min1 < min2) {
interval = min2 - max1;
normal.x = -normal.x;
normal.y = -normal.y;
} else {
interval = min1 - max2;
}
//exit early if positive
if (interval >= 0) {
return false;
}
if (MTV === null || interval > MTV) {
MTV = interval;
MN = {
x: normal.x,
y: normal.y
};
}
}
//loop through the edges of Polygon 2
for (i = 0; i < k; i++) {
nextPoint = points2[(i == k - 1 ? 0 : i + 1)];
currentPoint = points2[i];
//generate the normal for the current edge
normal.x = -(nextPoint[1] - currentPoint[1]);
normal.y = (nextPoint[0] - currentPoint[0]);
//normalize the vector
length = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
normal.x /= length;
normal.y /= length;
//default min max
min1 = min2 = -1;
max1 = max2 = -1;
//project all vertices from poly1 onto axis
for (j = 0; j < l; ++j) {
dot = points1[j][0] * normal.x + points1[j][1] * normal.y;
if (dot > max1 || max1 === -1) max1 = dot;
if (dot < min1 || min1 === -1) min1 = dot;
}
//project all vertices from poly2 onto axis
for (j = 0; j < k; ++j) {
dot = points2[j][0] * normal.x + points2[j][1] * normal.y;
if (dot > max2 || max2 === -1) max2 = dot;
if (dot < min2 || min2 === -1) min2 = dot;
}
//calculate the minimum translation vector should be negative
if (min1 < min2) {
interval = min2 - max1;
normal.x = -normal.x;
normal.y = -normal.y;
} else {
interval = min1 - max2;
}
//exit early if positive
if (interval >= 0) {
return false;
}
if (MTV === null || interval > MTV) MTV = interval;
if (interval > MTV2 || MTV2 === null) {
MTV2 = interval;
MN = {
x: normal.x,
y: normal.y
};
}
}
return {
overlap: MTV2,
normal: MN
};
}
});
Crafty.c("FPS",{
values:[],
maxValues:60,
init:function(){
this.bind("MessureFPS",function(fps){
if(this.values.length > this.maxValues) this.values.splice(0,1);
this.values.push(fps.value);
});
}
});
/**@
* #.WiredHitBox
* @comp Collision
*
* Components to display Crafty.polygon Array for debugging collision detection
*
* @example
* This will display a wired square over your original Canvas screen
* ~~~
* Crafty.e("2D,DOM,Player,Collision,WiredHitBox").collision(new Crafty.polygon([0,0],[0,300],[300,300],[300,0]))
* ~~~
*/
Crafty.c("WiredHitBox", {
init: function () {
if (Crafty.support.canvas) {
var c = document.getElementById('HitBox');
if (!c) {
c = document.createElement("canvas");
c.id = 'HitBox';
c.width = Crafty.viewport.width;
c.height = Crafty.viewport.height;
c.style.position = 'absolute';
c.style.left = "0px";
c.style.top = "0px";
c.style.zIndex = '1000';
Crafty.stage.elem.appendChild(c);
}
var ctx = c.getContext('2d');
var drawed = 0, total = Crafty("WiredHitBox").length;
this.requires("Collision").bind("EnterFrame", function () {
if (drawed == total) {
ctx.clearRect(0, 0, Crafty.viewport.width, Crafty.viewport.height);
drawed = 0;
}
ctx.beginPath();
for (var p in this.map.points) {
ctx.lineTo(Crafty.viewport.x + this.map.points[p][0], Crafty.viewport.y + this.map.points[p][1]);
}
ctx.closePath();
ctx.stroke();
drawed++;
});
}
return this;
}
});
/**@
* #.SolidHitBox
* @comp Collision
*
* Components to display Crafty.polygon Array for debugging collision detection
*
* @example
* This will display a solid triangle over your original Canvas screen
* ~~~
* Crafty.e("2D,DOM,Player,Collision,SolidHitBox").collision(new Crafty.polygon([0,0],[0,300],[300,300]))
* ~~~
*/
Crafty.c("SolidHitBox", {
init: function () {
if (Crafty.support.canvas) {
var c = document.getElementById('HitBox');
if (!c) {
c = document.createElement("canvas");
c.id = 'HitBox';
c.width = Crafty.viewport.width;
c.height = Crafty.viewport.height;
c.style.position = 'absolute';
c.style.left = "0px";
c.style.top = "0px";
c.style.zIndex = '1000';
Crafty.stage.elem.appendChild(c);
}
var ctx = c.getContext('2d');
var drawed = 0, total = Crafty("SolidHitBox").length;
this.requires("Collision").bind("EnterFrame", function () {
if (drawed == total) {
ctx.clearRect(0, 0, Crafty.viewport.width, Crafty.viewport.height);
drawed = 0;
}
ctx.beginPath();
for (var p in this.map.points) {
ctx.lineTo(Crafty.viewport.x + this.map.points[p][0], Crafty.viewport.y + this.map.points[p][1]);
}
ctx.closePath();
ctx.fill();
drawed++;
});
}
return this;
}
}); /**@
* #DOM
* @category Graphics
* Draws entities as DOM nodes, specifically `<DIV>`s.
*/
Crafty.c("DOM", {
/**@
* #._element
* @comp DOM
* The DOM element used to represent the entity.
*/
_element: null,
_interpolation:0,
init: function () {
this._element = document.createElement("div");
Crafty.stage.inner.appendChild(this._element);
this._element.style.position = "absolute";
this._element.id = "ent" + this[0];
this.bind("Change", function () {
if (!this._changed) {
this._changed = true;
Crafty.DrawManager.add(this);
}
});
function updateClass() {
var i = 0, c = this.__c, str = "";
for (i in c) {
str += ' ' + i;
}
str = str.substr(1);
this._element.className = str;
}
this.bind("NewComponent", updateClass).bind("RemoveComponent", updateClass);
if (Crafty.support.prefix === "ms" && Crafty.support.version < 9) {
this._filters = {};
this.bind("Rotate", function (e) {
var m = e.matrix,
elem = this._element.style,
M11 = m.M11.toFixed(8),
M12 = m.M12.toFixed(8),
M21 = m.M21.toFixed(8),
M22 = m.M22.toFixed(8);
this._filters.rotation = "progid:DXImageTransform.Microsoft.Matrix(M11=" + M11 + ", M12=" + M12 + ", M21=" + M21 + ", M22=" + M22 + ",sizingMethod='auto expand')";
});
}
this.bind("Remove", this.undraw);
this.bind("RemoveComponent", function (compName) {
if (compName === "DOM")
this.undraw();
});
},
/**@
* #.getDomId
* @comp DOM
* @sign public this .getId()
*
* Get the Id of the DOM element used to represent the entity.
*/
getDomId: function() {
return this._element.id;
},
/**@
* #.DOM
* @comp DOM
* @trigger Draw - when the entity is ready to be drawn to the stage - { style:String, type:"DOM", co}
* @sign public this .DOM(HTMLElement elem)
* @param elem - HTML element that will replace the dynamically created one
*
* Pass a DOM element to use rather than one created. Will set `._element` to this value. Removes the old element.
*/
DOM: function (elem) {
if (elem && elem.nodeType) {
this.undraw();
this._element = elem;
this._element.style.position = 'absolute';
}
return this;
},
/**@
* #.draw
* @comp DOM
* @sign public this .draw(void)
*
* Updates the CSS properties of the node to draw on the stage.
*/
draw: function () {
var style = this._element.style,
coord = this.__coord || [0, 0, 0, 0],
co = {
x: coord[0],
y: coord[1]
},
prefix = Crafty.support.prefix,
trans = [];
if (!this._visible) style.visibility = "hidden";
else style.visibility = "visible";
//utilize CSS3 if supported
if (Crafty.support.css3dtransform) {
trans.push("translate3d(" + (~~this._x) + "px," + (~~this._y) + "px,0)");
} else {
style.left = ~~(this._x) + "px";
style.top = ~~(this._y) + "px";
}
style.width = ~~(this._w) + "px";
style.height = ~~(this._h) + "px";
style.zIndex = this._z;
style.opacity = this._alpha;
style[prefix + "Opacity"] = this._alpha;
//if not version 9 of IE
if (prefix === "ms" && Crafty.support.version < 9) {
//for IE version 8, use ImageTransform filter
if (Crafty.support.version === 8) {
this._filters.alpha = "progid:DXImageTransform.Microsoft.Alpha(Opacity=" + (this._alpha * 100) + ")"; // first!
//all other versions use filter
} else {
this._filters.alpha = "alpha(opacity=" + (this._alpha * 100) + ")";
}
}
if (this._mbr) {
var origin = this._origin.x + "px " + this._origin.y + "px";
style.transformOrigin = origin;
style[prefix + "TransformOrigin"] = origin;
if (Crafty.support.css3dtransform) trans.push("rotateZ(" + this._rotation + "deg)");
else trans.push("rotate(" + this._rotation + "deg)");
}
if (this._flipX) {
trans.push("scaleX(-1)");
if (prefix === "ms" && Crafty.support.version < 9) {
this._filters.flipX = "fliph";
}
}
if (this._flipY) {
trans.push("scaleY(-1)");
if (prefix === "ms" && Crafty.support.version < 9) {
this._filters.flipY = "flipv";
}
}
//apply the filters if IE
if (prefix === "ms" && Crafty.support.version < 9) {
this.applyFilters();
}
style.transform = trans.join(" ");
style[prefix + "Transform"] = trans.join(" ");
this.trigger("Draw", {
style: style,
type: "DOM",
co: co
});
return this;
},
applyFilters: function () {
this._element.style.filter = "";
var str = "";
for (var filter in this._filters) {
if (!this._filters.hasOwnProperty(filter)) continue;
str += this._filters[filter] + " ";
}
this._element.style.filter = str;
},
/**@
* #.undraw
* @comp DOM
* @sign public this .undraw(void)
*
* Removes the element from the stage.
*/
undraw: function () {
if (this._element) {
Crafty.stage.inner.removeChild(this._element);
}
return this;
},
/**@
* #.css
* @comp DOM
* @sign public * css(String property, String value)
* @param property - CSS property to modify
* @param value - Value to give the CSS property
* @sign public * css(Object map)
* @param map - Object where the key is the CSS property and the value is CSS value
*
* Apply CSS styles to the element.
*
* Can pass an object where the key is the style property and the value is style value.
*
* For setting one style, simply pass the style as the first argument and the value as the second.
*
* The notation can be CSS or JS (e.g. `text-align` or `textAlign`).
*
* To return a value, pass the property.
*
* @example
* ~~~
* this.css({'text-align', 'center', font: 'Arial'});
* this.css("textAlign", "center");
* this.css("text-align"); //returns center
* ~~~
*/
css: function (obj, value) {
var key,
elem = this._element,
val,
style = elem.style;
//if an object passed
if (typeof obj === "object") {
for (key in obj) {
if (!obj.hasOwnProperty(key)) continue;
val = obj[key];
if (typeof val === "number") val += 'px';
style[Crafty.DOM.camelize(key)] = val;
}
} else {
//if a value is passed, set the property
if (value) {
if (typeof value === "number") value += 'px';
style[Crafty.DOM.camelize(obj)] = value;
} else { //otherwise return the computed property
return Crafty.DOM.getStyle(elem, obj);
}
}
this.trigger("Change");
return this;
}
});
/**
* Fix IE6 background flickering
*/
try {
document.execCommand("BackgroundImageCache", false, true);
} catch (e) { }
Crafty.extend({
/**@
* #Crafty.DOM
* @category Graphics
*
* Collection of utilities for using the DOM.
*/
DOM: {
/**@
* #Crafty.DOM.window
* @comp Crafty.DOM
*
* Object with `width` and `height` values representing the width
* and height of the `window`.
*/
window: {
init: function () {
this.width = window.innerWidth || (window.document.documentElement.clientWidth || window.document.body.clientWidth);
this.height = window.innerHeight || (window.document.documentElement.clientHeight || window.document.body.clientHeight);
},
width: 0,
height: 0
},
/**@
* #Crafty.DOM.inner
* @comp Crafty.DOM
* @sign public Object Crafty.DOM.inner(HTMLElement obj)
* @param obj - HTML element to calculate the position
* @returns Object with `x` key being the `x` position, `y` being the `y` position
*
* Find a DOM elements position including
* padding and border.
*/
inner: function (obj) {
var rect = obj.getBoundingClientRect(),
x = rect.left + (window.pageXOffset ? window.pageXOffset : document.body.scrollLeft),
y = rect.top + (window.pageYOffset ? window.pageYOffset : document.body.scrollTop),
//border left
borderX = parseInt(this.getStyle(obj, 'border-left-width') || 0, 10) || parseInt(this.getStyle(obj, 'borderLeftWidth') || 0, 10) || 0,
borderY = parseInt(this.getStyle(obj, 'border-top-width') || 0, 10) || parseInt(this.getStyle(obj, 'borderTopWidth') || 0, 10) || 0;
x += borderX;
y += borderY;
return {
x: x,
y: y
};
},
/**@
* #Crafty.DOM.getStyle
* @comp Crafty.DOM
* @sign public Object Crafty.DOM.getStyle(HTMLElement obj, String property)
* @param obj - HTML element to find the style
* @param property - Style to return
*
* Determine the value of a style on an HTML element. Notation can be
* in either CSS or JS.
*/
getStyle: function (obj, prop) {
var result;
if (obj.currentStyle)
result = obj.currentStyle[this.camelize(prop)];
else if (window.getComputedStyle)
result = document.defaultView.getComputedStyle(obj, null).getPropertyValue(this.csselize(prop));
return result;
},
/**
* Used in the Zepto framework
*
* Converts CSS notation to JS notation
*/
camelize: function (str) {
return str.replace(/-+(.)?/g, function (match, chr){
return chr ? chr.toUpperCase() : ''
});
},
/**
* Converts JS notation to CSS notation
*/
csselize: function (str) {
return str.replace(/[A-Z]/g, function (chr){
return chr ? '-' + chr.toLowerCase() : ''
});
},
/**@
* #Crafty.DOM.translate
* @comp Crafty.DOM
* @sign public Object Crafty.DOM.translate(Number x, Number y)
* @param x - x position to translate
* @param y - y position to translate
* @return Object with x and y as keys and translated values
*
* Method will translate x and y positions to positions on the
* stage. Useful for mouse events with `e.clientX` and `e.clientY`.
*/
translate: function (x, y) {
return {
x: (x - Crafty.stage.x + document.body.scrollLeft + document.documentElement.scrollLeft - Crafty.viewport._x)/Crafty.viewport._zoom,
y: (y - Crafty.stage.y + document.body.scrollTop + document.documentElement.scrollTop - Crafty.viewport._y)/Crafty.viewport._zoom
}
}
}
});
/**@
* #HTML
* @category Graphics
* Component allow for insertion of arbitrary HTML into an entity
*/
Crafty.c("HTML", {
inner: '',
init: function () {
this.requires('2D, DOM');
},
/**@
* #.replace
* @comp HTML
* @sign public this .replace(String html)
* @param html - arbitrary html
*
* This method will replace the content of this entity with the supplied html
*
* @example
* Create a link
* ~~~
* Crafty.e("HTML")
* .attr({x:20, y:20, w:100, h:100})
* .replace("<a href='http://www.craftyjs.com'>Crafty.js</a>");
* ~~~
*/
replace: function (new_html) {
this.inner = new_html;
this._element.innerHTML = new_html;
return this;
},
/**@
* #.append
* @comp HTML
* @sign public this .append(String html)
* @param html - arbitrary html
*
* This method will add the supplied html in the end of the entity
*
* @example
* Create a link
* ~~~
* Crafty.e("HTML")
* .attr({x:20, y:20, w:100, h:100})
* .append("<a href='http://www.craftyjs.com'>Crafty.js</a>");
* ~~~
*/
append: function (new_html) {
this.inner += new_html;
this._element.innerHTML += new_html;
return this;
},
/**@
* #.prepend
* @comp HTML
* @sign public this .prepend(String html)
* @param html - arbitrary html
*
* This method will add the supplied html in the beginning of the entity
*
* @example
* Create a link
* ~~~
* Crafty.e("HTML")
* .attr({x:20, y:20, w:100, h:100})
* .prepend("<a href='http://www.craftyjs.com'>Crafty.js</a>");
* ~~~
*/
prepend: function (new_html) {
this.inner = new_html + this.inner;
this._element.innerHTML = new_html + this.inner;
return this;
}
}); /**@
* #Storage
* @category Utilities
* Utility to allow data to be saved to a permanent storage solution: IndexedDB, WebSql, localstorage or cookies
*/
/**@
* #.open
* @comp Storage
* @sign .open(String gameName)
* @param gameName - a machine readable string to uniquely identify your game
*
* Opens a connection to the database. If the best they have is localstorage or lower, it does nothing
*
* @example
* Open a database
* ~~~
* Crafty.storage.open('MyGame');
* ~~~
*/
/**@
* #.save
* @comp Storage
* @sign .save(String key, String type, Mixed data)
* @param key - A unique key for identifying this piece of data
* @param type - 'save' or 'cache'
* @param data - Some kind of data.
*
* Saves a piece of data to the database. Can be anything, although entities are preferred.
* For all storage methods but IndexedDB, the data will be serialized as a string
* During serialization, an entity's SaveData event will be triggered.
* Components should implement a SaveData handler and attach the necessary information to the passed object
*
* @example
* Saves an entity to the database
* ~~~
* var ent = Crafty.e("2D, DOM")
* .attr({x: 20, y: 20, w: 100, h:100});
* Crafty.storage.open('MyGame');
* Crafty.storage.save('MyEntity', 'save', ent);
* ~~~
*/
/**@
* #.load
* @comp Storage
* @sign .load(String key, String type)
* @param key - A unique key to search for
* @param type - 'save' or 'cache'
* @param callback - Do things with the data you get back
*
* Loads a piece of data from the database.
* Entities will be reconstructed from the serialized string
* @example
* Loads an entity from the database
* ~~~
* Crafty.storage.open('MyGame');
* Crafty.storage.load('MyEntity', 'save', function (data) { // do things });
* ~~~
*/
/**@
* #.getAllKeys
* @comp Storage
* @sign .getAllKeys(String type)
* @param type - 'save' or 'cache'
* Gets all the keys for a given type
* @example
* Gets all the save games saved
* ~~~
* Crafty.storage.open('MyGame');
* var saves = Crafty.storage.getAllKeys('save');
* ~~~
*/
/**@
* #.external
* @comp Storage
* @sign .external(String url)
* @param url - URL to an external to save games too
*
* Enables and sets the url for saving games to an external server
*
* @example
* Save an entity to an external server
* ~~~
* Crafty.storage.external('http://somewhere.com/server.php');
* Crafty.storage.open('MyGame');
* var ent = Crafty.e('2D, DOM')
* .attr({x: 20, y: 20, w: 100, h:100});
* Crafty.storage.save('save01', 'save', ent);
* ~~~
*/
/**@
* #SaveData event
* @comp Storage
* @param data - An object containing all of the data to be serialized
* @param prepare - The function to prepare an entity for serialization
*
* Any data a component wants to save when it's serialized should be added to this object.
* Straight attribute should be set in data.attr.
* Anything that requires a special handler should be set in a unique property.
*
* @example
* Saves the innerHTML of an entity
* ~~~
* Crafty.e("2D DOM").bind("SaveData", function (data, prepare) {
* data.attr.x = this.x;
* data.attr.y = this.y;
* data.dom = this.element.innerHTML;
* });
* ~~~
*/
/**@
* #LoadData event
* @param data - An object containing all the data that been saved
* @param process - The function to turn a string into an entity
*
* Handlers for processing any data that needs more than straight assignment
*
* Note that data stord in the .attr object is automatically added to the entity.
* It does not need to be handled here
*
* @example
* ~~~
* Sets the innerHTML from a saved entity
* Crafty.e("2D DOM").bind("LoadData", function (data, process) {
* this.element.innerHTML = data.dom;
* });
* ~~~
*/
Crafty.storage = (function () {
var db = null, url, gameName, timestamps = {};
/*
* Processes a retrieved object.
* Creates an entity if it is one
*/
function process(obj) {
if (obj.c) {
var d = Crafty.e(obj.c)
.attr(obj.attr)
.trigger('LoadData', obj, process);
return d;
}
else if (typeof obj == 'object') {
for (var prop in obj) {
obj[prop] = process(obj[prop]);
}
}
return obj;
}
function unserialize(str) {
if (typeof str != 'string') return null;
var data = (JSON ? JSON.parse(str) : eval('(' + str + ')'));
return process(data);
}
/* recursive function
* searches for entities in an object and processes them for serialization
*/
function prep(obj) {
if (obj.__c) {
// object is entity
var data = {
c: [],
attr: {}
};
obj.trigger("SaveData", data, prep);
for (var i in obj.__c) {
data.c.push(i);
}
data.c = data.c.join(', ');
obj = data;
}
else if (typeof obj == 'object') {
// recurse and look for entities
for (var prop in obj) {
obj[prop] = prep(obj[prop]);
}
}
return obj;
}
function serialize(e) {
if (JSON) {
var data = prep(e);
return JSON.stringify(data);
}
else {
alert("Crafty does not support saving on your browser. Please upgrade to a newer browser.");
return false;
}
}
// for saving a game to a central server
function external(setUrl) {
url = setUrl;
}
function openExternal() {
if (1 && typeof url == "undefined") return;
// get the timestamps for external saves and compare them to local
// if the external is newer, load it
var xml = new XMLHttpRequest();
xhr.open("POST", url);
xhr.onreadystatechange = function (evt) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
var data = eval("(" + xhr.responseText + ")");
for (var i in data) {
if (Crafty.storage.check(data[i].key, data[i].timestamp)) {
loadExternal(data[i].key);
}
}
}
}
}
xhr.send("mode=timestamps&game=" + gameName);
}
function saveExternal(key, data, ts) {
if (1 && typeof url == "undefined") return;
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.send("mode=save&key=" + key + "&data=" + encodeURIComponent(data) + "&ts=" + ts + "&game=" + gameName);
}
function loadExternal(key) {
if (1 && typeof url == "undefined") return;
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.onreadystatechange = function (evt) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
var data = eval("(" + xhr.responseText + ")");
Crafty.storage.save(key, 'save', data);
}
}
}
xhr.send("mode=load&key=" + key + "&game=" + gameName);
}
/**
* get timestamp
*/
function ts() {
var d = new Date();
return d.getTime();
}
// everyone names their object different. Fix that nonsense.
if (typeof indexedDB != 'object') {
if (typeof mozIndexedDB == 'object') {
window.indexedDB = mozIndexedDB;
}
if (typeof webkitIndexedDB == 'object') {
window.indexedDB = webkitIndexedDB;
window.IDBTransaction = webkitIDBTransaction;
}
}
if (typeof indexedDB == 'object') {
return {
open: function (gameName_n) {
gameName = gameName_n;
var stores = [];
if (arguments.length == 1) {
stores.push('save');
stores.push('cache');
}
else {
stores = arguments;
stores.shift();
stores.push('save');
stores.push('cache');
}
if (db == null) {
var request = indexedDB.open(gameName, "Database for " + gameName);
request.onsuccess = function (e) {
db = e.target.result;
createStores();
getTimestamps();
openExternal();
};
}
else {
createStores();
getTimestamps();
openExternal();
}
// get all the timestamps for existing keys
function getTimestamps() {
try {
var trans = db.transaction(['save'], IDBTransaction.READ),
store = trans.objectStore('save'),
request = store.getAll();
request.onsuccess = function (e) {
var i = 0, a = event.target.result, l = a.length;
for (; i < l; i++) {
timestamps[a[i].key] = a[i].timestamp;
}
};
}
catch (e) {
}
}
function createStores() {
var request = db.setVersion("1.0");
request.onsuccess = function (e) {
for (var i = 0; i < stores.length; i++) {
var st = stores[i];
if (db.objectStoreNames.contains(st)) continue;
db.createObjectStore(st, {
keyPath: "key"
});
}
};
}
},
save: function (key, type, data) {
if (db == null) {
setTimeout(function () {
Crafty.storage.save(key, type, data);
}, 1);
return;
}
var str = serialize(data), t = ts();
if (type == 'save') saveExternal(key, str, t);
try {
var trans = db.transaction([type], IDBTransaction.READ_WRITE),
store = trans.objectStore(type),
request = store.put({
"data": str,
"timestamp": t,
"key": key
});
}
catch (e) {
console.error(e);
}
},
load: function (key, type, callback) {
if (db == null) {
setTimeout(function () {
Crafty.storage.load(key, type, callback);
}, 1);
return;
}
try {
var trans = db.transaction([type], IDBTransaction.READ),
store = trans.objectStore(type),
request = store.get(key);
request.onsuccess = function (e) {
callback(unserialize(e.target.result.data));
};
}
catch (e) {
console.error(e);
}
},
getAllKeys: function (type, callback) {
if (db == null) {
setTimeout(function () {
Crafty.storage.getAllkeys(type, callback);
}, 1);
}
try {
var trans = db.transaction([type], IDBTransaction.READ),
store = trans.objectStore(type),
request = store.getCursor(),
res = [];
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
res.push(cursor.key);
// 'continue' is a reserved word, so .continue() causes IE8 to completely bark with "SCRIPT1010: Expected identifier".
cursor['continue']();
}
else {
callback(res);
}
};
}
catch (e) {
console.error(e);
}
},
check: function (key, timestamp) {
return (timestamps[key] > timestamp);
},
external: external
};
}
else if (typeof openDatabase == 'function') {
return {
open: function (gameName_n) {
gameName = gameName_n;
if (arguments.length == 1) {
db = {
save: openDatabase(gameName_n + '_save', '1.0', 'Saves games for ' + gameName_n, 5 * 1024 * 1024),
cache: openDatabase(gameName_n + '_cache', '1.0', 'Cache for ' + gameName_n, 5 * 1024 * 1024)
}
}
else {
// allows for any other types that can be thought of
var args = arguments, i = 0;
args.shift();
for (; i < args.length; i++) {
if (typeof db[args[i]] == 'undefined')
db[args[i]] = openDatabase(gameName + '_' + args[i], '1.0', type, 5 * 1024 * 1024);
}
}
db['save'].transaction(function (tx) {
tx.executeSql('SELECT key, timestamp FROM data', [], function (tx, res) {
var i = 0, a = res.rows, l = a.length;
for (; i < l; i++) {
timestamps[a.item(i).key] = a.item(i).timestamp;
}
});
});
},
save: function (key, type, data) {
if (typeof db[type] == 'undefined' && gameName != '') {
this.open(gameName, type);
}
var str = serialize(data), t = ts();
if (type == 'save') saveExternal(key, str, t);
db[type].transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS data (key unique, text, timestamp)');
tx.executeSql('SELECT * FROM data WHERE key = ?', [key], function (tx, results) {
if (results.rows.length) {
tx.executeSql('UPDATE data SET text = ?, timestamp = ? WHERE key = ?', [str, t, key]);
}
else {
tx.executeSql('INSERT INTO data VALUES (?, ?, ?)', [key, str, t]);
}
});
});
},
load: function (key, type, callback) {
if (db[type] == null) {
setTimeout(function () {
Crafty.storage.load(key, type, callback);
}, 1);
return;
}
db[type].transaction(function (tx) {
tx.executeSql('SELECT text FROM data WHERE key = ?', [key], function (tx, results) {
if (results.rows.length) {
res = unserialize(results.rows.item(0).text);
callback(res);
}
});
});
},
getAllKeys: function (type, callback) {
if (db[type] == null) {
setTimeout(function () {
Crafty.storage.getAllKeys(type, callback);
}, 1);
return;
}
db[type].transaction(function (tx) {
tx.executeSql('SELECT key FROM data', [], function (tx, results) {
callback(results.rows);
});
});
},
check: function (key, timestamp) {
return (timestamps[key] > timestamp);
},
external: external
};
}
else if (typeof window.localStorage == 'object') {
return {
open: function (gameName_n) {
gameName = gameName_n;
},
save: function (key, type, data) {
var k = gameName + '.' + type + '.' + key,
str = serialize(data),
t = ts();
if (type == 'save') saveExternal(key, str, t);
window.localStorage[k] = str;
if (type == 'save')
window.localStorage[k + '.ts'] = t;
},
load: function (key, type, callback) {
var k = gameName + '.' + type + '.' + key,
str = window.localStorage[k];
callback(unserialize(str));
},
getAllKeys: function (type, callback) {
var res = {}, output = [], header = gameName + '.' + type;
for (var i in window.localStorage) {
if (i.indexOf(header) != -1) {
var key = i.replace(header, '').replace('.ts', '');
res[key] = true;
}
}
for (i in res) {
output.push(i);
}
callback(output);
},
check: function (key, timestamp) {
var ts = window.localStorage[gameName + '.save.' + key + '.ts'];
return (parseInt(timestamp) > parseInt(ts));
},
external: external
};
}
else {
// default fallback to cookies
return {
open: function (gameName_n) {
gameName = gameName_n;
},
save: function (key, type, data) {
// cookies are very limited in space. we can only keep saves there
if (type != 'save') return;
var str = serialize(data), t = ts();
if (type == 'save') saveExternal(key, str, t);
document.cookie = gameName + '_' + key + '=' + str + '; ' + gameName + '_' + key + '_ts=' + t + '; expires=Thur, 31 Dec 2099 23:59:59 UTC; path=/';
},
load: function (key, type, callback) {
if (type != 'save') return;
var reg = new RegExp(gameName + '_' + key + '=[^;]*'),
result = reg.exec(document.cookie),
data = unserialize(result[0].replace(gameName + '_' + key + '=', ''));
callback(data);
},
getAllKeys: function (type, callback) {
if (type != 'save') return;
var reg = new RegExp(gameName + '_[^_=]', 'g'),
matches = reg.exec(document.cookie),
i = 0, l = matches.length, res = {}, output = [];
for (; i < l; i++) {
var key = matches[i].replace(gameName + '_', '');
res[key] = true;
}
for (i in res) {
output.push(i);
}
callback(output);
},
check: function (key, timestamp) {
var header = gameName + '_' + key + '_ts',
reg = new RegExp(header + '=[^;]'),
result = reg.exec(document.cookie),
ts = result[0].replace(header + '=', '');
return (parseInt(timestamp) > parseInt(ts));
},
external: external
};
}
/* template
return {
open: function (gameName) {
},
save: function (key, type, data) {
},
load: function (key, type, callback) {
},
}*/
})(); /**@
* #Crafty.support
* @category Misc, Core
* Determines feature support for what Crafty can do.
*/
(function testSupport() {
var support = Crafty.support = {},
ua = navigator.userAgent.toLowerCase(),
match = /(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(o)pera(?:.*version)?[ \/]([\w.]+)/.exec(ua) ||
/(ms)ie ([\w.]+)/.exec(ua) ||
/(moz)illa(?:.*? rv:([\w.]+))?/.exec(ua) || [],
mobile = /iPad|iPod|iPhone|Android|webOS|IEMobile/i.exec(ua);
/**@
* #Crafty.mobile
* @comp Crafty.device
*
* Determines if Crafty is running on mobile device.
*
* If Crafty.mobile is equal true Crafty does some things under hood:
* ~~~
* - set viewport on max device width and heigh
* - set Crafty.stage.fullscreen on true
* - hide window scrollbars
* ~~~
*
* @see Crafty.viewport
*/
if (mobile) Crafty.mobile = mobile[0];
/**@
* #Crafty.support.setter
* @comp Crafty.support
* Is `__defineSetter__` supported?
*/
support.setter = ('__defineSetter__' in this && '__defineGetter__' in this);
/**@
* #Crafty.support.defineProperty
* @comp Crafty.support
* Is `Object.defineProperty` supported?
*/
support.defineProperty = (function () {
if (!'defineProperty' in Object) return false;
try {
Object.defineProperty({}, 'x', {});
}
catch (e) {
return false
};
return true;
})();
/**@
* #Crafty.support.audio
* @comp Crafty.support
* Is HTML5 `Audio` supported?
*/
support.audio = ('Audio' in window);
/**@
* #Crafty.support.prefix
* @comp Crafty.support
* Returns the browser specific prefix (`Moz`, `O`, `ms`, `webkit`).
*/
support.prefix = (match[1] || match[0]);
//browser specific quirks
if (support.prefix === "moz") support.prefix = "Moz";
if (support.prefix === "o") support.prefix = "O";
if (match[2]) {
/**@
* #Crafty.support.versionName
* @comp Crafty.support
* Version of the browser
*/
support.versionName = match[2];
/**@
* #Crafty.support.version
* @comp Crafty.support
* Version number of the browser as an Integer (first number)
*/
support.version = +(match[2].split("."))[0];
}
/**@
* #Crafty.support.canvas
* @comp Crafty.support
* Is the `canvas` element supported?
*/
support.canvas = ('getContext' in document.createElement("canvas"));
/**@
* #Crafty.support.webgl
* @comp Crafty.support
* Is WebGL supported on the canvas element?
*/
if (support.canvas) {
var gl;
try {
gl = document.createElement("canvas").getContext("experimental-webgl");
gl.viewportWidth = canvas.width;
gl.viewportHeight = canvas.height;
}
catch (e) { }
support.webgl = !!gl;
}
else {
support.webgl = false;
}
/**@
* #Crafty.support.css3dtransform
* @comp Crafty.support
* Is css3Dtransform supported by browser.
*/
support.css3dtransform = (typeof document.createElement("div").style["Perspective"] !== "undefined")
|| (typeof document.createElement("div").style[support.prefix + "Perspective"] !== "undefined");
/**@
* #Crafty.support.deviceorientation
* @comp Crafty.support
* Is deviceorientation event supported by browser.
*/
support.deviceorientation = (typeof window.DeviceOrientationEvent !== "undefined") || (typeof window.OrientationEvent !== "undefined");
/**@
* #Crafty.support.devicemotion
* @comp Crafty.support
* Is devicemotion event supported by browser.
*/
support.devicemotion = (typeof window.DeviceMotionEvent !== "undefined");
})();
Crafty.extend({
zeroFill: function (number, width) {
width -= number.toString().length;
if (width > 0)
return new Array(width + (/\./.test(number) ? 2 : 1)).join('0') + number;
return number.toString();
},
/**@
* #Crafty.sprite
* @category Graphics
* @sign public this Crafty.sprite([Number tile], String url, Object map[, Number paddingX[, Number paddingY]])
* @param tile - Tile size of the sprite map, defaults to 1
* @param url - URL of the sprite image
* @param map - Object where the key is what becomes a new component and the value points to a position on the sprite map
* @param paddingX - Horizontal space inbetween tiles. Defaults to 0.
* @param paddingY - Vertical space inbetween tiles. Defaults to paddingX.
* Generates components based on positions in a sprite image to be applied to entities.
*
* Accepts a tile size, URL and map for the name of the sprite and it's position.
*
* The position must be an array containing the position of the sprite where index `0`
* is the `x` position, `1` is the `y` position and optionally `2` is the width and `3`
* is the height. If the sprite map has padding, pass the values for the `x` padding
* or `y` padding. If they are the same, just add one value.
*
* If the sprite image has no consistent tile size, `1` or no argument need be
* passed for tile size.
*
* Entities that add the generated components are also given a component called `Sprite`.
*
* @see Sprite
*/
sprite: function (tile, tileh, url, map, paddingX, paddingY,marginX,marginY) {
var spriteName, temp, x, y, w, h, img;
//if no tile value, default to 1
if (typeof tile === "string") {
marginY = marginX;
marginX = paddingY;
paddingY = paddingX;
paddingX = map;
map = tileh;
url = tile;
tile = 1;
tileh = 1;
}
if (typeof tileh == "string") {
marginY = marginX;
marginX = paddingY;
paddingY = paddingX;
paddingX = map;
map = url;
url = tileh;
tileh = tile;
}
//if no paddingY, use paddingX
if (!paddingY && paddingX) paddingY = paddingX;
paddingX = parseInt(paddingX || 0, 10); //just incase
paddingY = parseInt(paddingY || 0, 10);
//if no marginY, use marginX
if (typeof marginY == "undefined" && marginX) marginY = marginX;
marginX = parseInt(marginX || 0, 10); //just incase
marginY = parseInt(marginY || 0, 10);
img = Crafty.asset(url);
if (!img) {
img = new Image();
Crafty.asset(url, img);
img.onload = function () {
//all components with this img are now ready
for (spriteName in map) {
Crafty(spriteName).each(function () {
this.ready = true;
this.trigger("Change");
});
}
};
img.src = url;
}
for (spriteName in map) {
if (!map.hasOwnProperty(spriteName)) continue;
temp = map[spriteName];
x = temp[0] * (tile + paddingX);
y = temp[1] * (tileh + paddingY);
w = temp[2] * tile || tile;
h = temp[3] * tileh || tileh;
//generates sprite components for each tile in the map
Crafty.c(spriteName, {
ready: false,
__coord: [x, y, w, h],
init: function () {
this.requires("Sprite");
this.__trim = [0, 0, 0, 0];
this.__image = url;
this.__coord = [this.__coord[0], this.__coord[1], this.__coord[2], this.__coord[3]];
this.__tile = tile;
this.__tileh = tileh;
this.__padding = [paddingX, paddingY];
this.__margin = [marginX, marginY];
this.img = img;
//draw now
if (this.img.complete && this.img.width > 0) {
this.ready = true;
this.trigger("Change");
}
//set the width and height to the sprite size
this.w = this.__coord[2];
this.h = this.__coord[3];
//set the margin
this.x = marginX;
this.y = marginY;
}
});
}
return this;
},
cachedSprite: function (tile, tileh, url, map, paddingX, paddingY,marginX,marginY) {
var spriteName, temp, x, y, w, h, img;
//if no tile value, default to 1
if (typeof tile === "string") {
marginY = marginX;
marginX = paddingY;
paddingY = paddingX;
paddingX = map;
map = tileh;
url = tile;
tile = 1;
tileh = 1;
}
if (typeof tileh == "string") {
marginY = marginX;
marginX = paddingY;
paddingY = paddingX;
paddingX = map;
map = url;
url = tileh;
tileh = tile;
}
//if no paddingY, use paddingX
if (!paddingY && paddingX) paddingY = paddingX;
paddingX = parseInt(paddingX || 0, 10); //just incase
paddingY = parseInt(paddingY || 0, 10);
//if no marginY, use marginX
if (typeof marginY == "undefined" && marginX) marginY = marginX;
marginX = parseInt(marginX || 0, 10); //just incase
marginY = parseInt(marginY || 0, 10);
img = Crafty.asset(url);
if (!img) {
img = new Image();
Crafty.asset(url, img);
img.onload = function () {
//all components with this img are now ready
for (spriteName in map) {
Crafty(spriteName).each(function () {
this.ready = true;
this.trigger("Change");
});
}
};
img.src = url;
}
for (spriteName in map) {
if (!map.hasOwnProperty(spriteName)) continue;
temp = map[spriteName];
x = temp[0] * (tile + paddingX);
y = temp[1] * (tileh + paddingY);
w = temp[2] * tile || tile;
h = temp[3] * tileh || tileh;
//generates sprite components for each tile in the map
Crafty.c(spriteName, {
ready: false,
__coord: [x, y, w, h],
init: function () {
this.requires("CachedSprite");
this.__trim = [0, 0, 0, 0];
this.__image = url;
this.__coord = [this.__coord[0], this.__coord[1], this.__coord[2], this.__coord[3]];
this.__tile = tile;
this.__tileh = tileh;
this.__padding = [paddingX, paddingY];
this.__margin = [marginX, marginY];
this.img = img;
//draw now
if (this.img.complete && this.img.width > 0) {
this.ready = true;
this.trigger("Change");
}
//set the width and height to the sprite size
this.w = this.__coord[2];
this.h = this.__coord[3];
//set the margin
this.x = marginX;
this.y = marginY;
}
});
}
return this;
},
_events: {},
/**@
* #Crafty.addEvent
* @category Events, Misc
* @sign public this Crafty.addEvent(Object ctx, HTMLElement obj, String event, Function callback)
* @param ctx - Context of the callback or the value of `this`
* @param obj - Element to add the DOM event to
* @param event - Event name to bind to
* @param callback - Method to execute when triggered
*
* Adds DOM level 3 events to elements. The arguments it accepts are the call
* context (the value of `this`), the DOM element to attach the event to,
* the event name (without `on` (`click` rather than `onclick`)) and
* finally the callback method.
*
* If no element is passed, the default element will be `window.document`.
*
* Callbacks are passed with event data.
*
* @see Crafty.removeEvent
*/
addEvent: function (ctx, obj, type, callback) {
if (arguments.length === 3) {
callback = type;
type = obj;
obj = window.document;
}
//save anonymous function to be able to remove
var afn = function (e) {
var e = e || window.event;
if (typeof callback === 'function') {
callback.call(ctx, e);
}
},
id = ctx[0] || "";
if (!this._events[id + obj + type + callback]) this._events[id + obj + type + callback] = afn;
else return;
if (obj.attachEvent) { //IE
obj.attachEvent('on' + type, afn);
} else { //Everyone else
obj.addEventListener(type, afn, false);
}
},
/**@
* #Crafty.removeEvent
* @category Events, Misc
* @sign public this Crafty.removeEvent(Object ctx, HTMLElement obj, String event, Function callback)
* @param ctx - Context of the callback or the value of `this`
* @param obj - Element the event is on
* @param event - Name of the event
* @param callback - Method executed when triggered
*
* Removes events attached by `Crafty.addEvent()`. All parameters must
* be the same that were used to attach the event including a reference
* to the callback method.
*
* @see Crafty.addEvent
*/
removeEvent: function (ctx, obj, type, callback) {
if (arguments.length === 3) {
callback = type;
type = obj;
obj = window.document;
}
//retrieve anonymouse function
var id = ctx[0] || "",
afn = this._events[id + obj + type + callback];
if (afn) {
if (obj.detachEvent) {
obj.detachEvent('on' + type, afn);
} else obj.removeEventListener(type, afn, false);
delete this._events[id + obj + type + callback];
}
},
/**@
* #Crafty.background
* @category Graphics, Stage
* @sign public void Crafty.background(String value)
* @param style - Modify the background with a color or image
*
* This method is essentially a shortcut for adding a background
* style to the stage element.
*/
background: function (style) {
Crafty.stage.elem.style.background = style;
},
/**@
* #Crafty.viewport
* @category Stage
*
* Viewport is essentially a 2D camera looking at the stage. Can be moved which
* in turn will react just like a camera moving in that direction.
*/
viewport: {
/**@
* #Crafty.viewport.clampToEntities
* @comp Crafty.viewport
*
* Decides if the viewport functions should clamp to game entities.
* When set to `true` functions such as Crafty.viewport.mouselook() will not allow you to move the
* viewport over areas of the game that has no entities.
* For development it can be useful to set this to false.
*/
clampToEntities: true,
width: 0,
height: 0,
/**@
* #Crafty.viewport.x
* @comp Crafty.viewport
*
* Will move the stage and therefore every visible entity along the `x`
* axis in the opposite direction.
*
* When this value is set, it will shift the entire stage. This means that entity
* positions are not exactly where they are on screen. To get the exact position,
* simply add `Crafty.viewport.x` onto the entities `x` position.
*/
_x: 0,
/**@
* #Crafty.viewport.y
* @comp Crafty.viewport
*
* Will move the stage and therefore every visible entity along the `y`
* axis in the opposite direction.
*
* When this value is set, it will shift the entire stage. This means that entity
* positions are not exactly where they are on screen. To get the exact position,
* simply add `Crafty.viewport.y` onto the entities `y` position.
*/
_y: 0,
/**@
* #Crafty.viewport.scroll
* @comp Crafty.viewport
* @sign Crafty.viewport.scroll(String axis, Number v)
* @param axis - 'x' or 'y'
* @param v - The new absolute position on the axis
*
* Will move the viewport to the position given on the specified axis
*
* @example
* Will move the camera 500 pixels right of its initial position, in effect
* shifting everything in the viewport 500 pixels to the left.
*
* ~~~
* Crafty.viewport.scroll('_x', 500);
* ~~~
*/
scroll: function (axis, v) {
v = Math.floor(v);
var change = v - this[axis], //change in direction
context = Crafty.canvas.context,
style = Crafty.stage.inner.style,
canvas;
//update viewport and DOM scroll
this[axis] = v;
if (axis == '_x') {
if (context) context.translate(change, 0);
} else {
if (context) context.translate(0, change);
}
if (context) Crafty.DrawManager.drawAll();
style[axis == '_x' ? "left" : "top"] = v + "px";
},
rect: function () {
return {
_x: -this._x,
_y: -this._y,
_w: this.width,
_h: this.height
};
},
/**@
* #Crafty.viewport.pan
* @comp Crafty.viewport
* @sign public void Crafty.viewport.pan(String axis, Number v, Number time)
* @param String axis - 'x' or 'y'. The axis to move the camera on
* @param Number v - the distance to move the camera by
* @param Number time - The duration in frames for the entire camera movement
*
* Pans the camera a given number of pixels over a given number of frames
*/
pan: (function () {
var tweens = {}, i, bound = false;
function enterFrame(e) {
var l = 0;
for (i in tweens) {
var prop = tweens[i];
if (prop.remTime > 0) {
prop.current += prop.diff;
prop.remTime--;
Crafty.viewport[i] = Math.floor(prop.current);
l++;
}
else {
delete tweens[i];
}
}
if (l) Crafty.viewport._clamp();
}
return function (axis, v, time) {
Crafty.viewport.follow();
if (axis == 'reset') {
for (i in tweens) {
tweens[i].remTime = 0;
}
return;
}
if (time == 0) time = 1;
tweens[axis] = {
diff: -v / time,
current: Crafty.viewport[axis],
remTime: time
};
if (!bound) {
Crafty.bind("EnterFrame", enterFrame);
bound = true;
}
}
})(),
/**@
* #Crafty.viewport.follow
* @comp Crafty.viewport
* @sign public void Crafty.viewport.follow(Object target, Number offsetx, Number offsety)
* @param Object target - An entity with the 2D component
* @param Number offsetx - Follow target should be offsetx pixels away from center
* @param Number offsety - Positive puts targ to the right of center
*
* Follows a given entity with the 2D component. If following target will take a portion of
* the viewport out of bounds of the world, following will stop until the target moves away.
*
* @example
* ~~~
* var ent = Crafty.e('2D, DOM').attr({w: 100, h: 100:});
* Crafty.viewport.follow(ent, 0, 0);
* ~~~
*/
follow: (function () {
var oldTarget, offx, offy;
function change() {
Crafty.viewport.scroll('_x', -(this.x + (this.w / 2) - (Crafty.viewport.width / 2) - offx));
Crafty.viewport.scroll('_y', -(this.y + (this.h / 2) - (Crafty.viewport.height / 2) - offy));
Crafty.viewport._clamp();
}
return function (target, offsetx, offsety) {
if (oldTarget)
oldTarget.unbind('Change', change);
if (!target || !target.has('2D'))
return;
Crafty.viewport.pan('reset');
oldTarget = target;
offx = (typeof offsetx != 'undefined') ? offsetx : 0;
offy = (typeof offsety != 'undefined') ? offsety : 0;
target.bind('Change', change);
change.call(target);
}
})(),
/**@
* #Crafty.viewport.centerOn
* @comp Crafty.viewport
* @sign public void Crafty.viewport.centerOn(Object target, Number time)
* @param Object target - An entity with the 2D component
* @param Number time - The number of frames to perform the centering over
*
* Centers the viewport on the given entity
*/
centerOn: function (targ, time) {
var x = targ.x,
y = targ.y,
mid_x = targ.w / 2,
mid_y = targ.h / 2,
cent_x = Crafty.viewport.width / 2,
cent_y = Crafty.viewport.height / 2,
new_x = x + mid_x - cent_x,
new_y = y + mid_y - cent_y;
Crafty.viewport.pan('reset');
Crafty.viewport.pan('x', new_x, time);
Crafty.viewport.pan('y', new_y, time);
},
/**@
* #Crafty.viewport._zoom
* @comp Crafty.viewport
*
* This value keeps an amount of viewport zoom, required for calculating mouse position at entity
*/
_zoom : 1,
/**@
* #Crafty.viewport.zoom
* @comp Crafty.viewport
* @sign public void Crafty.viewport.zoom(Number amt, Number cent_x, Number cent_y, Number time)
* @param Number amt - amount to zoom in on the target by (eg. 2, 4, 0.5)
* @param Number cent_x - the center to zoom on
* @param Number cent_y - the center to zoom on
* @param Number time - the duration in frames of the entire zoom operation
*
* Zooms the camera in on a given point. amt > 1 will bring the camera closer to the subject
* amt < 1 will bring it farther away. amt = 0 will do nothing.
* Zooming is multiplicative. To reset the zoom amount, pass 0.
*/
zoom: (function () {
var zoom = 1,
zoom_tick = 0,
dur = 0,
prop = Crafty.support.prefix + "Transform",
bound = false,
act = {},
prct = {};
// what's going on:
// 1. Get the original point as a percentage of the stage
// 2. Scale the stage
// 3. Get the new size of the stage
// 4. Get the absolute position of our point using previous percentage
// 4. Offset inner by that much
function enterFrame() {
if (dur > 0) {
var old = {
width: act.width * zoom,
height: act.height * zoom
};
zoom += zoom_tick;
this._zoom = zoom;
var new_s = {
width: act.width * zoom,
height: act.height * zoom
},
diff = {
width: new_s.width - old.width,
height: new_s.height - old.height
};
Crafty.stage.inner.style[prop] = 'scale(' + zoom + ',' + zoom + ')';
if (Crafty.canvas._canvas) {
Crafty.canvas.context.scale(zoom, zoom);
Crafty.DrawManager.drawAll();
}
Crafty.viewport.x -= diff.width * prct.width;
Crafty.viewport.y -= diff.height * prct.height;
dur--;
}
}
return function (amt, cent_x, cent_y, time) {
var bounds = Crafty.map.boundaries(),
final_zoom = amt ? zoom * amt : 1;
act.width = bounds.max.x - bounds.min.x;
act.height = bounds.max.y - bounds.min.y;
prct.width = cent_x / act.width;
prct.height = cent_y / act.height;
if (time == 0) time = 1;
zoom_tick = (final_zoom - zoom) / time;
dur = time;
Crafty.viewport.pan('reset');
if (!bound) {
Crafty.bind('EnterFrame', enterFrame);
bound = true;
}
}
})(),
/**@
* #Crafty.viewport.scale
* @comp Crafty.viewport
* @sign public void Crafty.viewport.scale(Number amt)
* @param Number amt - amount to zoom/scale in on the element on the viewport by (eg. 2, 4, 0.5)
*
* Zooms/scale the camera. amt > 1 increase all entities on stage
* amt < 1 will reduce all entities on stage. amt = 0 will reset the zoom/scale.
* Zooming/scaling is multiplicative. To reset the zoom/scale amount, pass 0.
*
* @example
* ~~~
* Crafty.viewport.scale(2); //to see effect add some entities on stage.
* ~~~
*/
scale: (function () {
var prop = Crafty.support.prefix + "Transform",
act = {};
return function (amt) {
var bounds = Crafty.map.boundaries(),
final_zoom = amt ? this._zoom * amt : 1;
this._zoom = final_zoom;
act.width = bounds.max.x - bounds.min.x;
act.height = bounds.max.y - bounds.min.y;
var new_s = {
width: act.width * final_zoom,
height: act.height * final_zoom
}
Crafty.viewport.pan('reset');
Crafty.stage.inner.style[prop] = 'scale(' + this._zoom + ',' + this._zoom + ')';
Crafty.stage.elem.style.width = new_s.width + "px";
Crafty.stage.elem.style.height = new_s.height + "px";
if (Crafty.canvas._canvas) {
Crafty.canvas._canvas.width = new_s.width;
Crafty.canvas._canvas.height = new_s.height;
Crafty.canvas.context.scale(this._zoom, this._zoom);
Crafty.DrawManager.drawAll();
}
Crafty.viewport.width = new_s.width;
Crafty.viewport.height = new_s.height;
}
})(),
/**@
* #Crafty.viewport.mouselook
* @comp Crafty.viewport
* @sign public void Crafty.viewport.mouselook(Boolean active)
* @param Boolean active - Activate or deactivate mouselook
*
* Toggle mouselook on the current viewport.
* Simply call this function and the user will be able to
* drag the viewport around.
*/
mouselook: (function () {
var active = false,
dragging = false,
lastMouse = {}
old = {};
return function (op, arg) {
if (typeof op == 'boolean') {
active = op;
if (active) {
Crafty.mouseObjs++;
}
else {
Crafty.mouseObjs = Math.max(0, Crafty.mouseObjs - 1);
}
return;
}
if (!active) return;
switch (op) {
case 'move':
case 'drag':
if (!dragging) return;
diff = {
x: arg.clientX - lastMouse.x,
y: arg.clientY - lastMouse.y
};
Crafty.viewport.x += diff.x;
Crafty.viewport.y += diff.y;
Crafty.viewport._clamp();
case 'start':
lastMouse.x = arg.clientX;
lastMouse.y = arg.clientY;
dragging = true;
break;
case 'stop':
dragging = false;
break;
}
};
})(),
_clamp: function () {
// clamps the viewport to the viewable area
// under no circumstances should the viewport see something outside the boundary of the 'world'
if (!this.clampToEntities) return;
var bound = Crafty.map.boundaries();
if (bound.max.x - bound.min.x > Crafty.viewport.width) {
bound.max.x -= Crafty.viewport.width;
if (Crafty.viewport.x < -bound.max.x) {
Crafty.viewport.x = -bound.max.x;
}
else if (Crafty.viewport.x > -bound.min.x) {
Crafty.viewport.x = -bound.min.x;
}
}
else {
Crafty.viewport.x = -1 * (bound.min.x + (bound.max.x - bound.min.x) / 2 - Crafty.viewport.width / 2);
}
if (bound.max.y - bound.min.y > Crafty.viewport.height) {
bound.max.y -= Crafty.viewport.height;
if (Crafty.viewport.y < -bound.max.y) {
Crafty.viewport.y = -bound.max.y;
}
else if (Crafty.viewport.y > -bound.min.y) {
Crafty.viewport.y = -bound.min.y;
}
}
else {
Crafty.viewport.y = -1 * (bound.min.y + (bound.max.y - bound.min.y) / 2 - Crafty.viewport.height / 2);
}
},
/**@
* #Crafty.viewport.init
* @comp Crafty.viewport
* @sign public void Crafty.viewport.init([Number width, Number height])
* @param width - Width of the viewport
* @param height - Height of the viewport
*
* Initialize the viewport. If the arguments 'width' or 'height' are missing, or Crafty.mobile is true, use Crafty.DOM.window.width and Crafty.DOM.window.height (full screen model).
* Create a div with id `cr-stage`, if there is not already an HTMLElement with id `cr-stage` (by `Crafty.viewport.init`).
*
* @see Crafty.device, Crafty.DOM, Crafty.stage
*/
init: function (w, h) {
Crafty.DOM.window.init();
//fullscreen if mobile or not specified
this.width = (!w || Crafty.mobile) ? Crafty.DOM.window.width : w;
this.height = (!h || Crafty.mobile) ? Crafty.DOM.window.height : h;
//check if stage exists
var crstage = document.getElementById("cr-stage");
/**@
* #Crafty.stage
* @category Core
* The stage where all the DOM entities will be placed.
*/
/**@
* #Crafty.stage.elem
* @comp Crafty.stage
* The `#cr-stage` div element.
*/
/**@
* #Crafty.stage.inner
* @comp Crafty.stage
* `Crafty.stage.inner` is a div inside the `#cr-stage` div that holds all DOM entities.
* If you use canvas, a `canvas` element is created at the same level in the dom
* as the the `Crafty.stage.inner` div. So the hierarchy in the DOM is
*
* `Crafty.stage.elem`
* <!-- not sure how to do indentation in the document-->
*
* - `Crafty.stage.inner` (a div HTMLElement)
*
* - `Crafty.canvas._canvas` (a canvas HTMLElement)
*/
//create stage div to contain everything
Crafty.stage = {
x: 0,
y: 0,
fullscreen: false,
elem: (crstage ? crstage : document.createElement("div")),
inner: document.createElement("div")
};
//fullscreen, stop scrollbars
if ((!w && !h) || Crafty.mobile) {
document.body.style.overflow = "hidden";
Crafty.stage.fullscreen = true;
}
Crafty.addEvent(this, window, "resize", Crafty.viewport.reload);
Crafty.addEvent(this, window, "blur", function () {
if (Crafty.settings.get("autoPause")) {
Crafty.pause();
}
});
Crafty.addEvent(this, window, "focus", function () {
if (Crafty._paused && Crafty.settings.get("autoPause")) {
Crafty.pause();
}
});
//make the stage unselectable
Crafty.settings.register("stageSelectable", function (v) {
Crafty.stage.elem.onselectstart = v ? function () {
return true;
} : function () {
return false;
};
});
Crafty.settings.modify("stageSelectable", false);
//make the stage have no context menu
Crafty.settings.register("stageContextMenu", function (v) {
Crafty.stage.elem.oncontextmenu = v ? function () {
return true;
} : function () {
return false;
};
});
Crafty.settings.modify("stageContextMenu", false);
Crafty.settings.register("autoPause", function (){ });
Crafty.settings.modify("autoPause", false);
//add to the body and give it an ID if not exists
if (!crstage) {
document.body.appendChild(Crafty.stage.elem);
Crafty.stage.elem.id = "cr-stage";
}
var elem = Crafty.stage.elem.style,
offset;
Crafty.stage.elem.appendChild(Crafty.stage.inner);
Crafty.stage.inner.style.position = "absolute";
Crafty.stage.inner.style.zIndex = "1";
//css style
elem.width = this.width + "px";
elem.height = this.height + "px";
elem.overflow = "hidden";
if (Crafty.mobile) {
elem.position = "absolute";
elem.left = "0px";
elem.top = "0px";
var meta = document.createElement("meta"),
head = document.getElementsByTagName("HEAD")[0];
//stop mobile zooming and scrolling
meta.setAttribute("name", "viewport");
meta.setAttribute("content", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no");
head.appendChild(meta);
//hide the address bar
meta = document.createElement("meta");
meta.setAttribute("name", "apple-mobile-web-app-capable");
meta.setAttribute("content", "yes");
head.appendChild(meta);
setTimeout(function () {
window.scrollTo(0, 1);
}, 0);
Crafty.addEvent(this, window, "touchmove", function (e) {
e.preventDefault();
});
Crafty.stage.x = 0;
Crafty.stage.y = 0;
} else {
elem.position = "relative";
//find out the offset position of the stage
offset = Crafty.DOM.inner(Crafty.stage.elem);
Crafty.stage.x = offset.x;
Crafty.stage.y = offset.y;
}
if (Crafty.support.setter) {
//define getters and setters to scroll the viewport
this.__defineSetter__('x', function (v) {
this.scroll('_x', v);
});
this.__defineSetter__('y', function (v) {
this.scroll('_y', v);
});
this.__defineGetter__('x', function () {
return this._x;
});
this.__defineGetter__('y', function () {
return this._y;
});
//IE9
} else if (Crafty.support.defineProperty) {
Object.defineProperty(this, 'x', {
set: function (v) {
this.scroll('_x', v);
},
get: function () {
return this._x;
}
});
Object.defineProperty(this, 'y', {
set: function (v) {
this.scroll('_y', v);
},
get: function () {
return this._y;
}
});
} else {
//create empty entity waiting for enterframe
this.x = this._x;
this.y = this._y;
Crafty.e("viewport");
}
},
/**@
* #Crafty.viewport.reload
* @comp Crafty.stage
*
* @sign public Crafty.viewport.reload()
*
* Recalculate and reload stage width, height and position.
* Useful when browser return wrong results on init (like safari on Ipad2).
*
*/
reload : function () {
Crafty.DOM.window.init();
var w = Crafty.DOM.window.width,
h = Crafty.DOM.window.height,
offset;
if (Crafty.stage.fullscreen) {
this.width = w;
this.height = h;
Crafty.stage.elem.style.width = w + "px";
Crafty.stage.elem.style.height = h + "px";
if (Crafty.canvas._canvas) {
Crafty.canvas._canvas.width = w;
Crafty.canvas._canvas.height = h;
Crafty.DrawManager.drawAll();
}
}
offset = Crafty.DOM.inner(Crafty.stage.elem);
Crafty.stage.x = offset.x;
Crafty.stage.y = offset.y;
}
},
/**@
* #Crafty.keys
* @category Input
* Object of key names and the corresponding key code.
*
* ~~~
* BACKSPACE: 8,
* TAB: 9,
* ENTER: 13,
* PAUSE: 19,
* CAPS: 20,
* ESC: 27,
* SPACE: 32,
* PAGE_UP: 33,
* PAGE_DOWN: 34,
* END: 35,
* HOME: 36,
* LEFT_ARROW: 37,
* UP_ARROW: 38,
* RIGHT_ARROW: 39,
* DOWN_ARROW: 40,
* INSERT: 45,
* DELETE: 46,
* 0: 48,
* 1: 49,
* 2: 50,
* 3: 51,
* 4: 52,
* 5: 53,
* 6: 54,
* 7: 55,
* 8: 56,
* 9: 57,
* A: 65,
* B: 66,
* C: 67,
* D: 68,
* E: 69,
* F: 70,
* G: 71,
* H: 72,
* I: 73,
* J: 74,
* K: 75,
* L: 76,
* M: 77,
* N: 78,
* O: 79,
* P: 80,
* Q: 81,
* R: 82,
* S: 83,
* T: 84,
* U: 85,
* V: 86,
* W: 87,
* X: 88,
* Y: 89,
* Z: 90,
* NUMPAD_0: 96,
* NUMPAD_1: 97,
* NUMPAD_2: 98,
* NUMPAD_3: 99,
* NUMPAD_4: 100,
* NUMPAD_5: 101,
* NUMPAD_6: 102,
* NUMPAD_7: 103,
* NUMPAD_8: 104,
* NUMPAD_9: 105,
* MULTIPLY: 106,
* ADD: 107,
* SUBSTRACT: 109,
* DECIMAL: 110,
* DIVIDE: 111,
* F1: 112,
* F2: 113,
* F3: 114,
* F4: 115,
* F5: 116,
* F6: 117,
* F7: 118,
* F8: 119,
* F9: 120,
* F10: 121,
* F11: 122,
* F12: 123,
* SHIFT: 16,
* CTRL: 17,
* ALT: 18,
* PLUS: 187,
* COMMA: 188,
* MINUS: 189,
* PERIOD: 190
* ~~~
*/
keys: {
'BACKSPACE': 8,
'TAB': 9,
'ENTER': 13,
'PAUSE': 19,
'CAPS': 20,
'ESC': 27,
'SPACE': 32,
'PAGE_UP': 33,
'PAGE_DOWN': 34,
'END': 35,
'HOME': 36,
'LEFT_ARROW': 37,
'UP_ARROW': 38,
'RIGHT_ARROW': 39,
'DOWN_ARROW': 40,
'INSERT': 45,
'DELETE': 46,
'0': 48,
'1': 49,
'2': 50,
'3': 51,
'4': 52,
'5': 53,
'6': 54,
'7': 55,
'8': 56,
'9': 57,
'A': 65,
'B': 66,
'C': 67,
'D': 68,
'E': 69,
'F': 70,
'G': 71,
'H': 72,
'I': 73,
'J': 74,
'K': 75,
'L': 76,
'M': 77,
'N': 78,
'O': 79,
'P': 80,
'Q': 81,
'R': 82,
'S': 83,
'T': 84,
'U': 85,
'V': 86,
'W': 87,
'X': 88,
'Y': 89,
'Z': 90,
'NUMPAD_0': 96,
'NUMPAD_1': 97,
'NUMPAD_2': 98,
'NUMPAD_3': 99,
'NUMPAD_4': 100,
'NUMPAD_5': 101,
'NUMPAD_6': 102,
'NUMPAD_7': 103,
'NUMPAD_8': 104,
'NUMPAD_9': 105,
'MULTIPLY': 106,
'ADD': 107,
'SUBSTRACT': 109,
'DECIMAL': 110,
'DIVIDE': 111,
'F1': 112,
'F2': 113,
'F3': 114,
'F4': 115,
'F5': 116,
'F6': 117,
'F7': 118,
'F8': 119,
'F9': 120,
'F10': 121,
'F11': 122,
'F12': 123,
'SHIFT': 16,
'CTRL': 17,
'ALT': 18,
'PLUS': 187,
'COMMA': 188,
'MINUS': 189,
'PERIOD': 190
},
/**@
* #Crafty.mouseButtons
* @category Input
* Object of mouseButton names and the corresponding button ID.
* In all mouseEvents we add the e.mouseButton property with a value normalized to match e.button of modern webkit
*
* ~~~
* LEFT: 0,
* MIDDLE: 1,
* RIGHT: 2
* ~~~
*/
mouseButtons: {
LEFT: 0,
MIDDLE: 1,
RIGHT: 2
}
});
/**
* Entity fixes the lack of setter support
*/
Crafty.c("viewport", {
init: function () {
this.bind("EnterFrame", function () {
if (Crafty.viewport._x !== Crafty.viewport.x) {
Crafty.viewport.scroll('_x', Crafty.viewport.x);
}
if (Crafty.viewport._y !== Crafty.viewport.y) {
Crafty.viewport.scroll('_y', Crafty.viewport.y);
}
});
}
});
Crafty.extend({
/**@
* #Crafty.device
* @category Misc
*/
device : {
_deviceOrientationCallback : false,
_deviceMotionCallback : false,
/**
* The HTML5 DeviceOrientation event returns three pieces of data:
* * alpha the direction the device is facing according to the compass
* * beta the angle in degrees the device is tilted front-to-back
* * gamma the angle in degrees the device is tilted left-to-right.
* * The angles values increase as you tilt the device to the right or towards you.
*
* Since Firefox uses the MozOrientationEvent which returns similar data but
* using different parameters and a different measurement system, we want to
* normalize that before we pass it to our _deviceOrientationCallback function.
*
* @param eventData HTML5 DeviceOrientation event
*/
_normalizeDeviceOrientation : function(eventData) {
var data;
if (window.DeviceOrientationEvent) {
data = {
// gamma is the left-to-right tilt in degrees, where right is positive
'tiltLR' : eventData.gamma,
// beta is the front-to-back tilt in degrees, where front is positive
'tiltFB' : eventData.beta,
// alpha is the compass direction the device is facing in degrees
'dir' : eventData.alpha,
// deviceorientation does not provide this data
'motUD' : null
}
} else if (window.OrientationEvent) {
data = {
// x is the left-to-right tilt from -1 to +1, so we need to convert to degrees
'tiltLR' : eventData.x * 90,
// y is the front-to-back tilt from -1 to +1, so we need to convert to degrees
// We also need to invert the value so tilting the device towards us (forward)
// results in a positive value.
'tiltFB' : eventData.y * -90,
// MozOrientation does not provide this data
'dir' : null,
// z is the vertical acceleration of the device
'motUD' : eventData.z
}
}
Crafty.device._deviceOrientationCallback(data);
},
/**
* @param eventData HTML5 DeviceMotion event
*/
_normalizeDeviceMotion : function(eventData) {
var acceleration = eventData.accelerationIncludingGravity,
facingUp = (acceleration.z > 0) ? +1 : -1;
var data = {
// Grab the acceleration including gravity from the results
'acceleration' : acceleration,
'rawAcceleration' : "["+ Math.round(acceleration.x) +", "+Math.round(acceleration.y) + ", " + Math.round(acceleration.z) + "]",
// Z is the acceleration in the Z axis, and if the device is facing up or down
'facingUp' : facingUp,
// Convert the value from acceleration to degrees acceleration.x|y is the
// acceleration according to gravity, we'll assume we're on Earth and divide
// by 9.81 (earth gravity) to get a percentage value, and then multiply that
// by 90 to convert to degrees.
'tiltLR' : Math.round(((acceleration.x) / 9.81) * -90),
'tiltFB' : Math.round(((acceleration.y + 9.81) / 9.81) * 90 * facingUp)
};
Crafty.device._deviceMotionCallback(data);
},
/**@
* #Crafty.device.deviceOrientation
* @comp Crafty.device
* @sign public Crafty.device.deviceOrientation(Function callback)
* @param callback - Callback method executed once as soon as device orientation is change
*
* Do something with normalized device orientation data:
* ~~~
* {
* 'tiltLR' : 'gamma the angle in degrees the device is tilted left-to-right.',
* 'tiltFB' : 'beta the angle in degrees the device is tilted front-to-back',
* 'dir' : 'alpha the direction the device is facing according to the compass',
* 'motUD' : 'The angles values increase as you tilt the device to the right or towards you.'
* }
* ~~~
*
* @example
* ~~~
* // Get DeviceOrientation event normalized data.
* Crafty.device.deviceOrientation(function(data){
* console.log('data.tiltLR : '+Math.round(data.tiltLR)+', data.tiltFB : '+Math.round(data.tiltFB)+', data.dir : '+Math.round(data.dir)+', data.motUD : '+data.motUD+'');
* });
* ~~~
*
* See browser support at http://caniuse.com/#search=device orientation.
*/
deviceOrientation : function(func) {
this._deviceOrientationCallback = func;
if (Crafty.support.deviceorientation) {
if (window.DeviceOrientationEvent) {
// Listen for the deviceorientation event and handle DeviceOrientationEvent object
Crafty.addEvent(this, window, 'deviceorientation', this._normalizeDeviceOrientation);
} else if (window.OrientationEvent) {
// Listen for the MozOrientation event and handle OrientationData object
Crafty.addEvent(this, window, 'MozOrientation', this._normalizeDeviceOrientation)
}
}
},
/**@
* #Crafty.device.deviceMotion
* @comp Crafty.device
* @sign public Crafty.device.deviceMotion(Function callback)
* @param callback - Callback method executed once as soon as device motion is change
*
* Do something with normalized device motion data:
* ~~~
* {
* 'acceleration' : ' Grab the acceleration including gravity from the results',
* 'rawAcceleration' : 'Display the raw acceleration data',
* 'facingUp' : 'Z is the acceleration in the Z axis, and if the device is facing up or down',
* 'tiltLR' : 'Convert the value from acceleration to degrees. acceleration.x is the acceleration according to gravity, we'll assume we're on Earth and divide by 9.81 (earth gravity) to get a percentage value, and then multiply that by 90 to convert to degrees.',
* 'tiltFB' : 'Convert the value from acceleration to degrees.'
* }
* ~~~
*
* @example
* ~~~
* // Get DeviceMotion event normalized data.
* Crafty.device.deviceMotion(function(data){
* console.log('data.moAccel : '+data.rawAcceleration+', data.moCalcTiltLR : '+Math.round(data.tiltLR)+', data.moCalcTiltFB : '+Math.round(data.tiltFB)+'');
* });
* ~~~
*
* See browser support at http://caniuse.com/#search=motion.
*/
deviceMotion : function(func) {
this._deviceMotionCallback = func;
if (Crafty.support.devicemotion) {
if (window.DeviceMotionEvent) {
// Listen for the devicemotion event and handle DeviceMotionEvent object
Crafty.addEvent(this, window, 'devicemotion', this._normalizeDeviceMotion);
}
}
}
}
});
/**@
* #Sprite
* @category Graphics
* @trigger Change - when the sprites change
* Component for using tiles in a sprite map.
*/
Crafty.c("Sprite", {
__image: '',
/*
* #.__tile
* @comp Sprite
*
* Horizontal sprite tile size.
*/
__tile: 0,
/*
* #.__tileh
* @comp Sprite
*
* Vertical sprite tile size.
*/
__tileh: 0,
__padding: null,
__trim: null,
img: null,
//ready is changed to true in Crafty.sprite
ready: false,
init: function () {
this.__trim = [0, 0, 0, 0];
var draw = function (e) {
var co = e.co,
pos = e.pos,
context = e.ctx,x,y;
if (e.type === "canvas") {
//draw the image on the canvas element
context.drawImage(this.img, //image element
co.x, //x position on sprite
co.y, //y position on sprite
co.w, //width on sprite
co.h, //height on sprite
pos._x, //x position on canvas
pos._y, //y position on canvas
pos._w, //width on canvas
pos._h //height on canvas
);
} else if (e.type === "DOM") {
this._element.style.background = "url('" + this.__image + "') no-repeat -" + co.x + "px -" + co.y + "px";
}
};
this.bind("Draw", draw).bind("RemoveComponent", function (id) {
if (id === "Sprite") this.unbind("Draw", draw);
});
},
/**@
* #.sprite
* @comp Sprite
* @sign public this .sprite(Number x, Number y, Number w, Number h)
* @param x - X cell position
* @param y - Y cell position
* @param w - Width in cells
* @param h - Height in cells
*
* Uses a new location on the sprite map as its sprite.
*
* Values should be in tiles or cells (not pixels).
*
* @example
* ~~~
* Crafty.e("2D, DOM, Sprite")
* .sprite(0, 0, 2, 2);
* ~~~
*/
/**@
* #.__coord
* @comp Sprite
*
* The coordinate of the slide within the sprite in the format of [x, y, w, h].
*/
sprite: function (x, y, w, h) {
this.__coord = [x * this.__tile + this.__padding[0] + this.__trim[0],
y * this.__tileh + this.__padding[1] + this.__trim[1],
this.__trim[2] || w * this.__tile || this.__tile,
this.__trim[3] || h * this.__tileh || this.__tileh];
this.trigger("Change");
return this;
},
/**@
* #.crop
* @comp Sprite
* @sign public this .crop(Number x, Number y, Number w, Number h)
* @param x - Offset x position
* @param y - Offset y position
* @param w - New width
* @param h - New height
*
* If the entity needs to be smaller than the tile size, use this method to crop it.
*
* The values should be in pixels rather than tiles.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Sprite")
* .crop(40, 40, 22, 23);
* ~~~
*/
crop: function (x, y, w, h) {
var old = this._mbr || this.pos();
this.__trim = [];
this.__trim[0] = x;
this.__trim[1] = y;
this.__trim[2] = w;
this.__trim[3] = h;
this.__coord[0] += x;
this.__coord[1] += y;
this.__coord[2] = w;
this.__coord[3] = h;
this._w = w;
this._h = h;
this.trigger("Change", old);
return this;
}
});
/**@
* #Sprite
* @category Graphics
* @trigger Change - when the sprites change
* Component for using tiles in a sprite map.
*/
Crafty.c("CachedSprite", {
__image: '',
/*
* #.__tile
* @comp Sprite
*
* Horizontal sprite tile size.
*/
__tile: 0,
/*
* #.__tileh
* @comp Sprite
*
* Vertical sprite tile size.
*/
__tileh: 0,
__padding: null,
__trim: null,
img: null,
//ready is changed to true in Crafty.sprite
ready: false,
cacheCanvas:null,
init: function () {
this.__trim = [0, 0, 0, 0];
var draw = function (e) {
var co = e.co,
pos = e.pos,
context = e.ctx;
if (e.type === "canvas") {
if(!this.cacheCanvas) return;
context.drawImage(this.cacheCanvas,pos._x,pos._y);
/*
//draw the image on the canvas element
context.drawImage(this.img, //image element
co.x, //x position on sprite
co.y, //y position on sprite
co.w, //width on sprite
co.h, //height on sprite
pos._x, //x position on canvas
pos._y, //y position on canvas
pos._w, //width on canvas
pos._h //height on canvas
);*/
} else if (e.type === "DOM") {
this._element.style.background = "url('" + this.__image + "') no-repeat -" + co.x + "px -" + co.y + "px";
}
};
this.bind("Draw", draw).bind("RemoveComponent", function (id) {
if (id === "CachedSprite") this.unbind("Draw", draw);
}).bind("Change",function(){
if(!this.cacheCanvas && this.ready){
var co = this.__coord;
var c = this.cacheCanvas;
if(c == null) {
c=this.cacheCanvas = document.createElement("canvas");
}
var ctx = c.getContext("2d");
c.width = co[2];
c.height = co[3];
ctx.drawImage(this.img,co[0],co[1],co[2],co[3],0,0,co[2],co[3]);
}
});
},
/**@
* #.sprite
* @comp Sprite
* @sign public this .sprite(Number x, Number y, Number w, Number h)
* @param x - X cell position
* @param y - Y cell position
* @param w - Width in cells
* @param h - Height in cells
*
* Uses a new location on the sprite map as its sprite.
*
* Values should be in tiles or cells (not pixels).
*
* @example
* ~~~
* Crafty.e("2D, DOM, Sprite")
* .sprite(0, 0, 2, 2);
* ~~~
*/
/**@
* #.__coord
* @comp Sprite
*
* The coordinate of the slide within the sprite in the format of [x, y, w, h].
*/
sprite: function (x, y, w, h) {
this.__coord = [x * this.__tile + this.__padding[0] + this.__trim[0],
y * this.__tileh + this.__padding[1] + this.__trim[1],
this.__trim[2] || w * this.__tile || this.__tile,
this.__trim[3] || h * this.__tileh || this.__tileh];
this.trigger("Change");
return this;
},
/**@
* #.crop
* @comp Sprite
* @sign public this .crop(Number x, Number y, Number w, Number h)
* @param x - Offset x position
* @param y - Offset y position
* @param w - New width
* @param h - New height
*
* If the entity needs to be smaller than the tile size, use this method to crop it.
*
* The values should be in pixels rather than tiles.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Sprite")
* .crop(40, 40, 22, 23);
* ~~~
*/
crop: function (x, y, w, h) {
var old = this._mbr || this.pos();
this.__trim = [];
this.__trim[0] = x;
this.__trim[1] = y;
this.__trim[2] = w;
this.__trim[3] = h;
this.__coord[0] += x;
this.__coord[1] += y;
this.__coord[2] = w;
this.__coord[3] = h;
this._w = w;
this._h = h;
this.trigger("Change", old);
return this;
}
});
/**@
* #Canvas
* @category Graphics
* @trigger Draw - when the entity is ready to be drawn to the stage - {type: "canvas", pos, co, ctx}
* @trigger NoCanvas - if the browser does not support canvas
*
* Draws itself onto a canvas. Crafty.canvas.init() will be automatically called it is not called already (hence the canvas element dosen't exist).
*/
Crafty.c("Canvas", {
_interpolation:0,
init: function () {
if (!Crafty.canvas.context) {
Crafty.canvas.init();
}
//increment the amount of canvas objs
Crafty.DrawManager.total2D++;
this.bind("Change", function (e) {
//if within screen, add to list
if (this._changed === false) {
this._changed = Crafty.DrawManager.add(e || this, this);
} else {
if (e) this._changed = Crafty.DrawManager.add(e, this);
}
});
this.bind("Remove", function () {
Crafty.DrawManager.total2D--;
Crafty.DrawManager.add(this, this);
});
},
/**@
* #.draw
* @comp Canvas
* @sign public this .draw([[Context ctx, ]Number x, Number y, Number w, Number h])
* @param ctx - Canvas 2D context if drawing on another canvas is required
* @param x - X offset for drawing a segment
* @param y - Y offset for drawing a segment
* @param w - Width of the segement to draw
* @param h - Height of the segment to draw
*
* Method to draw the entity on the canvas element. Can pass rect values for redrawing a segment of the entity.
*/
draw: function (ctx, x, y, w, h) {
if (!this.ready) return;
if (arguments.length === 4) {
h = w;
w = y;
y = x;
x = ctx;
ctx = Crafty.canvas.context;
}
var pos = { //inlined pos() function, for speed
_x: (this._x + ((x * this._interpolation) || 0)),
_y: (this._y + ((y * this._interpolation) || 0)),
_w: (w || this._w),
_h: (h || this._h)
},
context = ctx || Crafty.canvas.context,
coord = this.__coord || [0, 0, 0, 0],
co = {
x: coord[0] + ((x * this._interpolation) || 0),
y: coord[1] + ((y * this._interpolation) || 0),
w: w || coord[2],
h: h || coord[3]
};
if (this._mbr) {
context.save();
context.translate(this._origin.x + this._x, this._origin.y + this._y);
pos._x = -this._origin.x;
pos._y = -this._origin.y;
context.rotate((this._rotation % 360) * (Math.PI / 180));
}
if(this._flipX || this._flipY) {
context.save();
context.scale((this._flipX ? -1 : 1), (this._flipY ? -1 : 1));
if(this._flipX) {
pos._x = -(pos._x + pos._w)
}
if(this._flipY) {
pos._y = -(pos._y + pos._h)
}
}
//draw with alpha
if (this._alpha < 1.0) {
var globalpha = context.globalAlpha;
context.globalAlpha = this._alpha;
}
this.trigger("Draw", {
type: "canvas",
pos: pos,
co: co,
ctx: context
});
if (this._mbr || (this._flipX || this._flipY)) {
context.restore();
}
if (globalpha) {
context.globalAlpha = globalpha;
}
return this;
}
});
/**@
* #Crafty.canvas
* @category Graphics
*
* Collection of methods to draw on canvas.
*/
Crafty.extend({
canvas: {
/**@
* #Crafty.canvas.context
* @comp Crafty.canvas
*
* This will return the 2D context of the main canvas element.
* The value returned from `Crafty.canvas._canvas.getContext('2d')`.
*/
context: null,
/**@
* #Crafty.canvas._canvas
* @comp Crafty.canvas
*
* Main Canvas element
*/
/**@
* #Crafty.canvas.init
* @comp Crafty.canvas
* @sign public void Crafty.canvas.init(void)
* @trigger NoCanvas - triggered if `Crafty.support.canvas` is false
*
* Creates a `canvas` element inside `Crafty.stage.elem`. Must be called
* before any entities with the Canvas component can be drawn.
*
* This method will automatically be called if no `Crafty.canvas.context` is
* found.
*/
init: function () {
//check if canvas is supported
if (!Crafty.support.canvas) {
Crafty.trigger("NoCanvas");
Crafty.stop();
return;
}
//create 3 empty canvas elements
var c;
c = document.createElement("canvas");
c.width = Crafty.viewport.width;
c.height = Crafty.viewport.height;
c.style.position = 'absolute';
c.style.left = "0px";
c.style.top = "0px";
Crafty.stage.elem.appendChild(c);
Crafty.canvas.context = c.getContext('2d');
Crafty.canvas._canvas = c;
}
}
});
Crafty.extend({
over: null, //object mouseover, waiting for out
mouseObjs: 0,
mousePos: {},
lastEvent: null,
keydown: {},
selected: false,
/**@
* #Crafty.keydown
* @category Input
* Remembering what keys (referred by Unicode) are down.
*
* @example
* ~~~
* Crafty.c("Keyboard", {
* isDown: function (key) {
* if (typeof key === "string") {
* key = Crafty.keys[key];
* }
* return !!Crafty.keydown[key];
* }
* });
* ~~~
* @see Keyboard, Crafty.keys
*/
detectBlur: function (e) {
var selected = ((e.clientX > Crafty.stage.x && e.clientX < Crafty.stage.x + Crafty.viewport.width) &&
(e.clientY > Crafty.stage.y && e.clientY < Crafty.stage.y + Crafty.viewport.height));
if (!Crafty.selected && selected)
Crafty.trigger("CraftyFocus");
if (Crafty.selected && !selected)
Crafty.trigger("CraftyBlur");
Crafty.selected = selected;
},
mouseDispatch: function (e) {
if (!Crafty.mouseObjs) return;
Crafty.lastEvent = e;
var maxz = -1,
closest,
q,
i = 0, l,
pos = Crafty.DOM.translate(e.clientX, e.clientY),
x, y,
dupes = {},
tar = e.target ? e.target : e.srcElement,
type = e.type;
//Normalize button according to http://unixpapa.com/js/mouse.html
if (e.which == null) {
e.mouseButton = (e.button < 2) ? Crafty.mouseButtons.LEFT : ((e.button == 4) ? Crafty.mouseButtons.MIDDLE : Crafty.mouseButtons.RIGHT);
} else {
e.mouseButton = (e.which < 2) ? Crafty.mouseButtons.LEFT : ((e.which == 2) ? Crafty.mouseButtons.MIDDLE : Crafty.mouseButtons.RIGHT);
}
e.realX = x = Crafty.mousePos.x = pos.x;
e.realY = y = Crafty.mousePos.y = pos.y;
//if it's a DOM element with Mouse component we are done
if (tar.nodeName != "CANVAS") {
while (typeof (tar.id) != 'string' && tar.id.indexOf('ent') == -1) {
tar = tar.parentNode;
}
ent = Crafty(parseInt(tar.id.replace('ent', '')))
if (ent.has('Mouse') && ent.isAt(x, y))
closest = ent;
}
//else we search for an entity with Mouse component
if (!closest) {
q = Crafty.map.search({
_x: x,
_y: y,
_w: 1,
_h: 1
}, false);
for (l = q.length; i < l; ++i) {
if (!q[i].__c.Mouse || !q[i]._visible) continue;
var current = q[i],
flag = false;
//weed out duplicates
if (dupes[current[0]]) continue;
else dupes[current[0]] = true;
if (current.mapArea) {
if (current.mapArea.containsPoint(x, y)) {
flag = true;
}
} else if (current.isAt(x, y)) flag = true;
if (flag && (current._z >= maxz || maxz === -1)) {
//if the Z is the same, select the closest GUID
if (current._z === maxz && current[0] < closest[0]) {
continue;
}
maxz = current._z;
closest = current;
}
}
}
//found closest object to mouse
if (closest) {
//click must mousedown and out on tile
if (type === "mousedown") {
closest.trigger("MouseDown", e);
} else if (type === "mouseup") {
closest.trigger("MouseUp", e);
} else if (type == "dblclick") {
closest.trigger("DoubleClick", e);
} else if (type == "click") {
closest.trigger("Click", e);
}else if (type === "mousemove") {
closest.trigger("MouseMove", e);
if (this.over !== closest) { //if new mousemove, it is over
if (this.over) {
this.over.trigger("MouseOut", e); //if over wasn't null, send mouseout
this.over = null;
}
this.over = closest;
closest.trigger("MouseOver", e);
}
} else closest.trigger(type, e); //trigger whatever it is
} else {
if (type === "mousemove" && this.over) {
this.over.trigger("MouseOut", e);
this.over = null;
}
if (type === "mousedown") {
Crafty.viewport.mouselook('start', e);
}
else if (type === "mousemove") {
Crafty.viewport.mouselook('drag', e);
}
else if (type == "mouseup") {
Crafty.viewport.mouselook('stop');
}
}
if (type === "mousemove") {
this.lastEvent = e;
}
},
/**@
* #Crafty.touchDispatch
* @category Input
*
* TouchEvents have a different structure then MouseEvents.
* The relevant data lives in e.changedTouches[0].
* To normalize TouchEvents we catch em and dispatch a mock MouseEvent instead.
*
* @see Crafty.mouseDispatch
*/
touchDispatch: function(e) {
var type;
if (e.type === "touchstart") type = "mousedown";
else if (e.type === "touchmove") type = "mousemove";
else if (e.type === "touchend") type = "mouseup";
else if (e.type === "touchcancel") type = "mouseup";
else if (e.type === "touchleave") type = "mouseup";
if(e.touches && e.touches.length) {
first = e.touches[0];
} else if(e.changedTouches && e.changedTouches.length) {
first = e.changedTouches[0];
}
var simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent(type, true, true, window, 1,
first.screenX,
first.screenY,
first.clientX,
first.clientY,
false, false, false, false, 0, e.relatedTarget
);
first.target.dispatchEvent(simulatedEvent);
},
/**@
* #KeyboardEvent
* @category Input
* Keyboard Event triggerd by Crafty Core
* @trigger KeyDown - is triggered for each entity when the DOM 'keydown' event is triggered.
* @trigger KeyUp - is triggered for each entity when the DOM 'keyup' event is triggered.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Color")
* .attr({x: 100, y: 100, w: 50, h: 50})
* .color("red")
* .bind('KeyDown', function(e) {
* if(e.key == Crafty.keys['LEFT_ARROW']) {
* this.x=this.x-1;
* } else if (e.key == Crafty.keys['RIGHT_ARROW']) {
* this.x=this.x+1;
* } else if (e.key == Crafty.keys['UP_ARROW']) {
* this.y=this.y-1;
* } else if (e.key == Crafty.keys['DOWN_ARROW']) {
* this.y=this.y+1;
* }
* });
* ~~~
*
* @see Crafty.keys
*/
/**@
* #Crafty.eventObject
* @category Input
*
* Event Object used in Crafty for cross browser compatiblity
*/
/**@
* #.key
* @comp Crafty.eventObject
*
* Unicode of the key pressed
*/
keyboardDispatch: function (e) {
e.key = e.keyCode || e.which;
if (e.type === "keydown") {
if (Crafty.keydown[e.key] !== true) {
Crafty.keydown[e.key] = true;
Crafty.trigger("KeyDown", e);
}
} else if (e.type === "keyup") {
delete Crafty.keydown[e.key];
Crafty.trigger("KeyUp", e);
}
//prevent default actions for all keys except backspace and F1-F12.
//Among others this prevent the arrow keys from scrolling the parent page
//of an iframe hosting the game
if(Crafty.selected && !(e.key == 8 || e.key >= 112 && e.key <= 135)) {
e.stopPropagation();
if(e.preventDefault) e.preventDefault();
else e.returnValue = false;
return false;
}
}
});
//initialize the input events onload
Crafty.bind("Load", function () {
Crafty.addEvent(this, "keydown", Crafty.keyboardDispatch);
Crafty.addEvent(this, "keyup", Crafty.keyboardDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "mousedown", Crafty.mouseDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "mouseup", Crafty.mouseDispatch);
Crafty.addEvent(this, document.body, "mouseup", Crafty.detectBlur);
Crafty.addEvent(this, Crafty.stage.elem, "mousemove", Crafty.mouseDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "click", Crafty.mouseDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "dblclick", Crafty.mouseDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "touchstart", Crafty.touchDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "touchmove", Crafty.touchDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "touchend", Crafty.touchDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "touchcancel", Crafty.touchDispatch);
Crafty.addEvent(this, Crafty.stage.elem, "touchleave", Crafty.touchDispatch);
});
/**@
* #Mouse
* @category Input
* Provides the entity with mouse related events
* @trigger MouseOver - when the mouse enters the entity - MouseEvent
* @trigger MouseOut - when the mouse leaves the entity - MouseEvent
* @trigger MouseDown - when the mouse button is pressed on the entity - MouseEvent
* @trigger MouseUp - when the mouse button is released on the entity - MouseEvent
* @trigger Click - when the user clicks the entity. [See documentation](http://www.quirksmode.org/dom/events/click.html) - MouseEvent
* @trigger DoubleClick - when the user double clicks the entity - MouseEvent
* @trigger MouseMove - when the mouse is over the entity and moves - MouseEvent
* Crafty adds the mouseButton property to MouseEvents that match one of
*
* ~~~
* - Crafty.mouseButtons.LEFT
* - Crafty.mouseButtons.RIGHT
* - Crafty.mouseButtons.MIDDLE
* ~~~
*
* @example
* ~~~
* myEntity.bind('Click', function() {
* console.log("Clicked!!");
* })
*
* myEntity.bind('Click', function(e) {
* if( e.mouseButton == Crafty.mouseButtons.RIGHT )
* console.log("Clicked right button");
* })
* ~~~
*/
Crafty.c("Mouse", {
init: function () {
Crafty.mouseObjs++;
this.bind("Remove", function () {
Crafty.mouseObjs--;
});
},
/**@
* #.areaMap
* @comp Mouse
* @sign public this .areaMap(Crafty.polygon polygon)
* @param polygon - Instance of Crafty.polygon used to check if the mouse coordinates are inside this region
* @sign public this .areaMap(Array point1, .., Array pointN)
* @param point# - Array with an `x` and `y` position to generate a polygon
*
* Assign a polygon to the entity so that mouse events will only be triggered if
* the coordinates are inside the given polygon.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Color, Mouse")
* .color("red")
* .attr({ w: 100, h: 100 })
* .bind('MouseOver', function() {console.log("over")})
* .areaMap([0,0], [50,0], [50,50], [0,50])
* ~~~
*
* @see Crafty.polygon
*/
areaMap: function (poly) {
//create polygon
if (arguments.length > 1) {
//convert args to array to create polygon
var args = Array.prototype.slice.call(arguments, 0);
poly = new Crafty.polygon(args);
}
poly.shift(this._x, this._y);
//this.map = poly;
this.mapArea = poly;
this.attach(this.mapArea);
return this;
}
});
/**@
* #Draggable
* @category Input
* Enable drag and drop of the entity.
* @trigger Dragging - is triggered each frame the entity is being dragged - MouseEvent
* @trigger StartDrag - is triggered when dragging begins - MouseEvent
* @trigger StopDrag - is triggered when dragging ends - MouseEvent
*/
Crafty.c("Draggable", {
_origMouseDOMPos: null,
_oldX: null,
_oldY: null,
_dragging: false,
_dir:null,
_ondrag: null,
_ondown: null,
_onup: null,
//Note: the code is note tested with zoom, etc., that may distort the direction between the viewport and the coordinate on the canvas.
init: function () {
this.requires("Mouse");
this._ondrag = function (e) {
var pos = Crafty.DOM.translate(e.clientX, e.clientY);
// ignore invalid 0 0 position - strange problem on ipad
if (pos.x == 0 || pos.y == 0) {
return false;
}
if(this._dir) {
var len = (pos.x - this._origMouseDOMPos.x) * this._dir.x + (pos.y - this._origMouseDOMPos.y) * this._dir.y;
this.x = this._oldX + len * this._dir.x;
this.y = this._oldY + len * this._dir.y;
} else {
this.x = this._oldX + (pos.x - this._origMouseDOMPos.x);
this.y = this._oldY + (pos.y - this._origMouseDOMPos.y);
}
this.trigger("Dragging", e);
};
this._ondown = function (e) {
if (e.mouseButton !== Crafty.mouseButtons.LEFT) return;
//start drag
this._origMouseDOMPos = Crafty.DOM.translate(e.clientX, e.clientY);
this._oldX = this._x;
this._oldY = this._y;
this._dragging = true;
Crafty.addEvent(this, Crafty.stage.elem, "mousemove", this._ondrag);
Crafty.addEvent(this, Crafty.stage.elem, "mouseup", this._onup);
this.trigger("StartDrag", e);
};
this._onup = function upper(e) {
if (this._dragging == true) {
Crafty.removeEvent(this, Crafty.stage.elem, "mousemove", this._ondrag);
Crafty.removeEvent(this, Crafty.stage.elem, "mouseup", this._onup);
this._dragging = false;
this.trigger("StopDrag", e);
}
};
this.enableDrag();
},
/**@
* #.dragDirection
* @comp Draggable
* @sign public this .dragDirection()
* Remove any previously specifed direction.
*
* @sign public this .dragDirection(vector)
* @param vector - Of the form of {x: valx, y: valy}, the vector (valx, valy) denotes the move direction.
*
* @sign public this .dragDirection(degree)
* @param degree - A number, the degree (clockwise) of the move direction with respect to the x axis.
* Specify the dragging direction.
*
* @example
* ~~~
* this.dragDirection()
* this.dragDirection({x:1, y:0}) //Horizonatal
* this.dragDirection({x:0, y:1}) //Vertical
* // Note: because of the orientation of x and y axis,
* // this is 45 degree clockwise with respect to the x axis.
* this.dragDirection({x:1, y:1}) //45 degree.
* this.dragDirection(60) //60 degree.
* ~~~
*/
dragDirection: function(dir) {
if (typeof dir === 'undefined') {
this._dir=null;
} else if (("" + parseInt(dir)) == dir) { //dir is a number
this._dir={
x: Math.cos(dir/180*Math.PI)
,
y: Math.sin(dir/180*Math.PI)
};
}
else {
var r=Math.sqrt(dir.x * dir.x + dir.y * dir.y)
this._dir={
x: dir.x/r
,
y: dir.y/r
};
}
},
/**@
* #.stopDrag
* @comp Draggable
* @sign public this .stopDrag(void)
* @trigger StopDrag - Called right after the mouse listeners are removed
*
* Stop the entity from dragging. Essentially reproducing the drop.
*
* @see .startDrag
*/
stopDrag: function () {
Crafty.removeEvent(this, Crafty.stage.elem, "mousemove", this._ondrag);
Crafty.removeEvent(this, Crafty.stage.elem, "mouseup", this._onup);
this._dragging = false;
this.trigger("StopDrag");
return this;
},
/**@
* #.startDrag
* @comp Draggable
* @sign public this .startDrag(void)
*
* Make the entity follow the mouse positions.
*
* @see .stopDrag
*/
startDrag: function () {
if (!this._dragging) {
this._dragging = true;
Crafty.addEvent(this, Crafty.stage.elem, "mousemove", this._ondrag);
}
return this;
},
/**@
* #.enableDrag
* @comp Draggable
* @sign public this .enableDrag(void)
*
* Rebind the mouse events. Use if `.disableDrag` has been called.
*
* @see .disableDrag
*/
enableDrag: function () {
this.bind("MouseDown", this._ondown);
Crafty.addEvent(this, Crafty.stage.elem, "mouseup", this._onup);
return this;
},
/**@
* #.disableDrag
* @comp Draggable
* @sign public this .disableDrag(void)
*
* Stops entity from being draggable. Reenable with `.enableDrag()`.
*
* @see .enableDrag
*/
disableDrag: function () {
this.unbind("MouseDown", this._ondown);
this.stopDrag();
return this;
}
});
/**@
* #Keyboard
* @category Input
* Give entities keyboard events (`keydown` and `keyup`).
*/
Crafty.c("Keyboard", {
/**@
* #.isDown
* @comp Keyboard
* @sign public Boolean isDown(String keyName)
* @param keyName - Name of the key to check. See `Crafty.keys`.
* @sign public Boolean isDown(Number keyCode)
* @param keyCode - Key code in `Crafty.keys`.
*
* Determine if a certain key is currently down.
*
* @example
* ~~~
* entity.requires('KeyBoard').bind('KeyDown', function () { if (this.isDown('SPACE')) jump(); });
* ~~~
*
* @see Crafty.keys
*/
isDown: function (key) {
if (typeof key === "string") {
key = Crafty.keys[key];
}
return !!Crafty.keydown[key];
}
});
/**@
* #Multiway
* @category Input
* Used to bind keys to directions and have the entity move acordingly
* @trigger NewDirection - triggered when direction changes - { x:Number, y:Number } - New direction
* @trigger Moved - triggered on movement on either x or y axis. If the entity has moved on both axes for diagonal movement the event is triggered twice - { x:Number, y:Number } - Old position
*/
Crafty.c("Multiway", {
_speed: 3,
_keydown: function (e) {
if (this._keys[e.key]) {
this._movement.x = Math.round((this._movement.x + this._keys[e.key].x) * 1000) / 1000;
this._movement.y = Math.round((this._movement.y + this._keys[e.key].y) * 1000) / 1000;
this.trigger('NewDirection', this._movement);
}
},
_keyup: function (e) {
if (this._keys[e.key]) {
this._movement.x = Math.round((this._movement.x - this._keys[e.key].x) * 1000) / 1000;
this._movement.y = Math.round((this._movement.y - this._keys[e.key].y) * 1000) / 1000;
this.trigger('NewDirection', this._movement);
}
},
_enterframe: function () {
if (this.disableControls) return;
if (this._movement.x !== 0) {
this.x += this._movement.x;
this.trigger('Moved', {
x: this.x - this._movement.x,
y: this.y
});
}
if (this._movement.y !== 0) {
this.y += this._movement.y;
this.trigger('Moved', {
x: this.x,
y: this.y - this._movement.y
});
}
},
init: function () {
this._keyDirection = {};
this._keys = {};
this._movement = {
x: 0,
y: 0
};
this._speed = {
x: 3,
y: 3
};
},
/**@
* #.multiway
* @comp Multiway
* @sign public this .multiway([Number speed,] Object keyBindings )
* @param speed - Amount of pixels to move the entity whilst a key is down
* @param keyBindings - What keys should make the entity go in which direction. Direction is specified in degrees
* Constructor to initialize the speed and keyBindings. Component will listen to key events and move the entity appropriately.
*
* When direction changes a NewDirection event is triggered with an object detailing the new direction: {x: x_movement, y: y_movement}
* When entity has moved on either x- or y-axis a Moved event is triggered with an object specifying the old position {x: old_x, y: old_y}
*
* @example
* ~~~
* this.multiway(3, {UP_ARROW: -90, DOWN_ARROW: 90, RIGHT_ARROW: 0, LEFT_ARROW: 180});
* this.multiway({x:3,y:1.5}, {UP_ARROW: -90, DOWN_ARROW: 90, RIGHT_ARROW: 0, LEFT_ARROW: 180});
* this.multiway({W: -90, S: 90, D: 0, A: 180});
* ~~~
*/
multiway: function (speed, keys) {
if (keys) {
if (speed.x && speed.y) {
this._speed.x = speed.x;
this._speed.y = speed.y;
} else {
this._speed.x = speed;
this._speed.y = speed;
}
} else {
keys = speed;
}
this._keyDirection = keys;
this.speed(this._speed);
this.enableControl();
//Apply movement if key is down when created
for (var k in keys) {
if (Crafty.keydown[Crafty.keys[k]]) {
this.trigger("KeyDown", {
key: Crafty.keys[k]
});
}
}
return this;
},
/**@
* #.enableControl
* @comp Multiway
* @sign public this .enableControl()
*
* Enable the component to listen to key events.
*
* @example
* ~~~
* this.enableControl();
* ~~~
*/
enableControl: function() {
this.bind("KeyDown", this._keydown)
.bind("KeyUp", this._keyup)
.bind("EnterFrame", this._enterframe);
return this;
},
/**@
* #.disableControl
* @comp Multiway
* @sign public this .disableControl()
*
* Disable the component to listen to key events.
*
* @example
* ~~~
* this.disableControl();
* ~~~
*/
disableControl: function() {
this.unbind("KeyDown", this._keydown)
.unbind("KeyUp", this._keyup)
.unbind("EnterFrame", this._enterframe);
return this;
},
speed: function (speed) {
for (var k in this._keyDirection) {
var keyCode = Crafty.keys[k] || k;
this._keys[keyCode] = {
x: Math.round(Math.cos(this._keyDirection[k] * (Math.PI / 180)) * 1000 * speed.x) / 1000,
y: Math.round(Math.sin(this._keyDirection[k] * (Math.PI / 180)) * 1000 * speed.y) / 1000
};
}
return this;
}
});
/**@
* #Fourway
* @category Input
* Move an entity in four directions by using the
* arrow keys or `W`, `A`, `S`, `D`.
*/
Crafty.c("Fourway", {
init: function () {
this.requires("Multiway");
},
/**@
* #.fourway
* @comp Fourway
* @sign public this .fourway(Number speed)
* @param speed - Amount of pixels to move the entity whilst a key is down
* Constructor to initialize the speed. Component will listen for key events and move the entity appropriately.
* This includes `Up Arrow`, `Right Arrow`, `Down Arrow`, `Left Arrow` as well as `W`, `A`, `S`, `D`.
*
* When direction changes a NewDirection event is triggered with an object detailing the new direction: {x: x_movement, y: y_movement}
* When entity has moved on either x- or y-axis a Moved event is triggered with an object specifying the old position {x: old_x, y: old_y}
*
* The key presses will move the entity in that direction by the speed passed in the argument.
*
* @see Multiway
*/
fourway: function (speed) {
this.multiway(speed, {
UP_ARROW: -90,
DOWN_ARROW: 90,
RIGHT_ARROW: 0,
LEFT_ARROW: 180,
W: -90,
S: 90,
D: 0,
A: 180,
Z: -90,
Q: 180
});
return this;
}
});
/**@
* #Twoway
* @category Input
* Move an entity left or right using the arrow keys or `D` and `A` and jump using up arrow or `W`.
*
* When direction changes a NewDirection event is triggered with an object detailing the new direction: {x: x_movement, y: y_movement}. This is consistent with Fourway and Multiway components.
* When entity has moved on x-axis a Moved event is triggered with an object specifying the old position {x: old_x, y: old_y}
*/
Crafty.c("Twoway", {
_speed: 3,
_up: false,
init: function () {
this.requires("Fourway, Keyboard");
},
/**@
* #.twoway
* @comp Twoway
* @sign public this .twoway(Number speed[, Number jumpSpeed])
* @param speed - Amount of pixels to move left or right
* @param jumpSpeed - How high the entity should jump
*
* Constructor to initialize the speed and power of jump. Component will
* listen for key events and move the entity appropriately. This includes
* ~~~
* `Up Arrow`, `Right Arrow`, `Left Arrow` as well as W, A, D. Used with the
* `gravity` component to simulate jumping.
* ~~~
*
* The key presses will move the entity in that direction by the speed passed in
* the argument. Pressing the `Up Arrow` or `W` will cause the entiy to jump.
*
* @see Gravity, Fourway
*/
twoway: function (speed, jump) {
this.multiway(speed, {
RIGHT_ARROW: 0,
LEFT_ARROW: 180,
D: 0,
A: 180,
Q: 180
});
if (speed) this._speed = speed;
jump = jump || this._speed * 2;
this.bind("EnterFrame", function () {
if (this.disableControls) return;
if (this._up) {
this.y -= jump;
this._falling = true;
}
}).bind("KeyDown", function () {
if (this.isDown("UP_ARROW") || this.isDown("W") || this.isDown("Z")) this._up = true;
});
return this;
}
});
/**@
* #SpriteAnimation
* @category Animation
* @trigger AnimationEnd - When the animation finishes - { reel }
* @trigger Change - On each frame
*
* Used to animate sprites by changing the sprites in the sprite map.
*
*/
Crafty.c("SpriteAnimation", {
/**@
* #._reels
* @comp SpriteAnimation
*
* A map consists of arrays that contains the coordinates of each frame within the sprite, e.g.,
* `{"walk_left":[[96,48],[112,48],[128,48]]}`
*/
_reels: null,
_frame: null,
/**@
* #._currentReelId
* @comp SpriteAnimation
*
* The current playing reel (one element of `this._reels`). It is `null` if no reel is playing.
*/
_currentReelId: null,
init: function () {
this._reels = {};
},
/**@
* #.animate
* @comp SpriteAnimation
* @sign public this .animate(String reelId, Number fromX, Number y, Number toX)
* @param reelId - ID of the animation reel being created
* @param fromX - Starting `x` position (in the unit of sprite horizontal size) on the sprite map
* @param y - `y` position on the sprite map (in the unit of sprite vertical size). Remains constant through the animation.
* @param toX - End `x` position on the sprite map (in the unit of sprite horizontal size)
* @sign public this .animate(String reelId, Array frames)
* @param reelId - ID of the animation reel being created
* @param frames - Array of arrays containing the `x` and `y` values: [[x1,y1],[x2,y2],...]
* @sign public this .animate(String reelId, Number duration[, Number repeatCount])
* @param reelId - ID of the animation reel to play
* @param duration - Play the animation within a duration (in frames)
* @param repeatCount - number of times to repeat the animation. Use -1 for infinitely
*
* Method to setup animation reels or play pre-made reels. Animation works by changing the sprites over
* a duration. Only works for sprites built with the Crafty.sprite methods. See the Tween component for animation of 2D properties.
*
* To setup an animation reel, pass the name of the reel (used to identify the reel and play it later), and either an
* array of absolute sprite positions or the start x on the sprite map, the y on the sprite map and then the end x on the sprite map.
*
* To play a reel, pass the name of the reel and the duration it should play for (in frames). If you need
* to repeat the animation, simply pass in the amount of times the animation should repeat. To repeat
* forever, pass in `-1`.
*
* @example
* ~~~
* Crafty.sprite(16, "images/sprite.png", {
* PlayerSprite: [0,0]
* });
*
* Crafty.e("2D, DOM, SpriteAnimation, PlayerSprite")
* .animate('PlayerRunning', 0, 0, 3) //setup animation
* .animate('PlayerRunning', 15, -1) // start animation
*
* Crafty.e("2D, DOM, SpriteAnimation, PlayerSprite")
* .animate('PlayerRunning', 0, 3, 0) //setup animation
* .animate('PlayerRunning', 15, -1) // start animation
* ~~~
*
* @see crafty.sprite
*/
animate: function (reelId, fromx, y, tox) {
var reel, i, tile, tileh, duration, pos;
//play a reel
//.animate('PlayerRunning', 15, -1) // start animation
if (arguments.length < 4 && typeof fromx === "number") {
duration = fromx;
//make sure not currently animating
this._currentReelId = reelId;
currentReel = this._reels[reelId];
this._frame = {
currentReel: currentReel,
numberOfFramesBetweenSlides: Math.ceil(duration / currentReel.length),
currentSlideNumber: 0,
frameNumberBetweenSlides: 0,
repeat: 0
};
if (arguments.length === 3 && typeof y === "number") {
//User provided repetition count
if (y === -1) this._frame.repeatInfinitly = true;
else this._frame.repeat = y;
}
pos = this._frame.currentReel[0];
this.__coord[0] = pos[0];
this.__coord[1] = pos[1];
this.bind("EnterFrame", this.updateSprite);
return this;
}
// .animate('PlayerRunning', 0, 0, 3) //setup animation
if (typeof fromx === "number") {
// Defind in Sprite component.
tile = this.__tile + parseInt(this.__padding[0] || 0, 10);
tileh = this.__tileh + parseInt(this.__padding[1] || 0, 10);
reel = [];
i = fromx;
if (tox > fromx) {
for (; i <= tox; i++) {
reel.push([i * tile, y * tileh]);
}
} else {
for (; i >= tox; i--) {
reel.push([i * tile, y * tileh]);
}
}
this._reels[reelId] = reel;
} else if (typeof fromx === "object") {
// @sign public this .animate(reelId, [[x1,y1],[x2,y2],...])
i = 0;
reel = [];
tox = fromx.length - 1;
tile = this.__tile + parseInt(this.__padding[0] || 0, 10);
tileh = this.__tileh + parseInt(this.__padding[1] || 0, 10);
for (; i <= tox; i++) {
pos = fromx[i];
reel.push([pos[0] * tile, pos[1] * tileh]);
}
this._reels[reelId] = reel;
}
return this;
},
/**@
* #.updateSprite
* @comp SpriteAnimation
* @sign private void .updateSprite()
*
* This is called at every `EnterFrame` event when `.animate()` enables animation. It update the SpriteAnimation component when the slide in the sprite should be updated.
*
* @example
* ~~~
* this.bind("EnterFrame", this.updateSprite);
* ~~~
*
* @see crafty.sprite
*/
updateSprite: function () {
var data = this._frame;
if (!data) {
return;
}
if (this._frame.frameNumberBetweenSlides++ === data.numberOfFramesBetweenSlides) {
var pos = data.currentReel[data.currentSlideNumber++];
this.__coord[0] = pos[0];
this.__coord[1] = pos[1];
this._frame.frameNumberBetweenSlides = 0;
}
if (data.currentSlideNumber === data.currentReel.length) {
if (this._frame.repeatInfinitly === true || this._frame.repeat > 0) {
if (this._frame.repeat) this._frame.repeat--;
this._frame.frameNumberBetweenSlides = 0;
this._frame.currentSlideNumber = 0;
} else {
if (this._frame.frameNumberBetweenSlides === data.numberOfFramesBetweenSlides) {
this.trigger("AnimationEnd", {
reel: data.currentReel
});
this.stop();
return;
}
}
}
this.trigger("Change");
},
/**@
* #.stop
* @comp SpriteAnimation
* @sign public this .stop(void)
*
* Stop any animation currently playing.
*/
stop: function () {
this.unbind("EnterFrame", this.updateSprite);
this.unbind("AnimationEnd");
this._currentReelId = null;
this._frame = null;
return this;
},
/**@
* #.reset
* @comp SpriteAnimation
* @sign public this .reset(void)
*
* Method will reset the entities sprite to its original.
*/
reset: function () {
if (!this._frame) return this;
var co = this._frame.currentReel[0];
this.__coord[0] = co[0];
this.__coord[1] = co[1];
this.stop();
return this;
},
/**@
* #.isPlaying
* @comp SpriteAnimation
* @sign public Boolean .isPlaying([String reelId])
* @param reelId - Determine if the animation reel with this reelId is playing.
*
* Determines if an animation is currently playing. If a reel is passed, it will determine
* if the passed reel is playing.
*
* @example
* ~~~
* myEntity.isPlaying() //is any animation playing
* myEntity.isPlaying('PlayerRunning') //is the PlayerRunning animation playing
* ~~~
*/
isPlaying: function (reelId) {
if (!reelId) return !!this._interval;
return this._currentReelId === reelId;
}
});
/**@
* #Tween
* @category Animation
* @trigger TweenEnd - when a tween finishes - String - property
*
* Component to animate the change in 2D properties over time.
*/
Crafty.c("Tween", {
_step: null,
_numProps: 0,
/**@
* #.tween
* @comp Tween
* @sign public this .tween(Object properties, Number duration)
* @param properties - Object of 2D properties and what they should animate to
* @param duration - Duration to animate the properties over (in frames)
*
* This method will animate a 2D entities properties over the specified duration.
* These include `x`, `y`, `w`, `h`, `alpha` and `rotation`.
*
* The object passed should have the properties as keys and the value should be the resulting
* values of the properties.
*
* @example
* Move an object to 100,100 and fade out in 200 frames.
* ~~~
* Crafty.e("2D, Tween")
* .attr({alpha: 1.0, x: 0, y: 0})
* .tween({alpha: 0.0, x: 100, y: 100}, 200)
* ~~~
*/
tween: function (props, duration) {
this.each(function () {
if (this._step == null) {
this._step = {};
this.bind('EnterFrame', tweenEnterFrame);
this.bind('RemoveComponent', function (c) {
if (c == 'Tween') {
this.unbind('EnterFrame', tweenEnterFrame);
}
});
}
for (var prop in props) {
this._step[prop] = {
prop: props[prop],
val: (props[prop] - this[prop]) / duration,
rem: duration
};
this._numProps++;
}
});
return this;
}
});
function tweenEnterFrame(e) {
if (this._numProps <= 0) return;
var prop, k;
for (k in this._step) {
prop = this._step[k];
this[k] += prop.val;
if (--prop.rem == 0) {
// decimal numbers rounding fix
this[k] = prop.prop;
this.trigger("TweenEnd", k);
// make sure the duration wasn't changed in TweenEnd
if (this._step[k].rem <= 0) {
delete this._step[k];
}
this._numProps--;
}
}
if (this.has('Mouse')) {
var over = Crafty.over,
mouse = Crafty.mousePos;
if (over && over[0] == this[0] && !this.isAt(mouse.x, mouse.y)) {
this.trigger('MouseOut', Crafty.lastEvent);
Crafty.over = null;
}
else if ((!over || over[0] != this[0]) && this.isAt(mouse.x, mouse.y)) {
Crafty.over = this;
this.trigger('MouseOver', Crafty.lastEvent);
}
}
}
/**@
* #Color
* @category Graphics
* Draw a solid color for the entity
*/
Crafty.c("Color", {
_color: "",
ready: true,
init: function () {
this.bind("Draw", function (e) {
if (e.type === "DOM") {
e.style.background = this._color;
e.style.lineHeight = 0;
} else if (e.type === "canvas") {
if (this._color) e.ctx.fillStyle = this._color;
e.ctx.fillRect(e.pos._x, e.pos._y, e.pos._w, e.pos._h);
}
});
},
/**@
* #.color
* @comp Color
* @trigger Change - when the color changes
* @sign public this .color(String color)
* @sign public String .color()
* @param color - Color of the rectangle
* Will create a rectangle of solid color for the entity, or return the color if no argument is given.
*
* The argument must be a color readable depending on which browser you
* choose to support. IE 8 and below doesn't support the rgb() syntax.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Color")
* .color("#969696");
* ~~~
*/
color: function (color) {
if (!color) return this._color;
this._color = color;
this.trigger("Change");
return this;
}
});
/**@
* #Tint
* @category Graphics
* Similar to Color by adding an overlay of semi-transparent color.
*
* *Note: Currently only works for Canvas*
*/
Crafty.c("Tint", {
_color: null,
_strength: 1.0,
init: function () {
var draw = function d(e) {
var context = e.ctx || Crafty.canvas.context;
context.fillStyle = this._color || "rgb(0,0,0)";
context.fillRect(e.pos._x, e.pos._y, e.pos._w, e.pos._h);
};
this.bind("Draw", draw).bind("RemoveComponent", function (id) {
if (id === "Tint") this.unbind("Draw", draw);
});
},
/**@
* #.tint
* @comp Tint
* @trigger Change - when the tint is applied
* @sign public this .tint(String color, Number strength)
* @param color - The color in hexidecimal
* @param strength - Level of opacity
*
* Modify the color and level opacity to give a tint on the entity.
*
* @example
* ~~~
* Crafty.e("2D, Canvas, Tint")
* .tint("#969696", 0.3);
* ~~~
*/
tint: function (color, strength) {
this._strength = strength;
this._color = Crafty.toRGB(color, this._strength);
this.trigger("Change");
return this;
}
});
/**@
* #Image
* @category Graphics
* Draw an image with or without repeating (tiling).
*/
Crafty.c("Image", {
_repeat: "repeat",
ready: false,
init: function () {
var draw = function (e) {
if (e.type === "canvas") {
//skip if no image
if (!this.ready || !this._pattern) return;
var context = e.ctx;
context.fillStyle = this._pattern;
context.save();
context.translate(e.pos._x, e.pos._y);
context.fillRect(0, 0, this._w, this._h);
context.restore();
} else if (e.type === "DOM") {
if (this.__image)
e.style.background = "url(" + this.__image + ") " + this._repeat;
}
};
this.bind("Draw", draw).bind("RemoveComponent", function (id) {
if (id === "Image") this.unbind("Draw", draw);
});
},
/**@
* #.image
* @comp Image
* @trigger Change - when the image is loaded
* @sign public this .image(String url[, String repeat])
* @param url - URL of the image
* @param repeat - If the image should be repeated to fill the entity.
*
* Draw specified image. Repeat follows CSS syntax (`"no-repeat", "repeat", "repeat-x", "repeat-y"`);
*
* *Note: Default repeat is `no-repeat` which is different to standard DOM (which is `repeat`)*
*
* If the width and height are `0` and repeat is set to `no-repeat` the width and
* height will automatically assume that of the image. This is an
* easy way to create an image without needing sprites.
*
* @example
* Will default to no-repeat. Entity width and height will be set to the images width and height
* ~~~
* var ent = Crafty.e("2D, DOM, Image").image("myimage.png");
* ~~~
* Create a repeating background.
* ~~~
* var bg = Crafty.e("2D, DOM, Image")
* .attr({w: Crafty.viewport.width, h: Crafty.viewport.height})
* .image("bg.png", "repeat");
* ~~~
*
* @see Crafty.sprite
*/
image: function (url, repeat) {
this.__image = url;
this._repeat = repeat || "no-repeat";
this.img = Crafty.asset(url);
if (!this.img) {
this.img = new Image();
Crafty.asset(url, this.img);
this.img.src = url;
var self = this;
this.img.onload = function () {
if (self.has("Canvas")) self._pattern = Crafty.canvas.context.createPattern(self.img, self._repeat);
self.ready = true;
if (self._repeat === "no-repeat") {
self.w = self.img.width;
self.h = self.img.height;
}
self.trigger("Change");
};
return this;
} else {
this.ready = true;
if (this.has("Canvas")) this._pattern = Crafty.canvas.context.createPattern(this.img, this._repeat);
if (this._repeat === "no-repeat") {
this.w = this.img.width;
this.h = this.img.height;
}
}
this.trigger("Change");
return this;
}
});
Crafty.extend({
_scenes: [],
_current: null,
/**@
* #Crafty.scene
* @category Scenes, Stage
* @trigger SceneChange - when a scene is played - { oldScene:String, newScene:String }
* @sign public void Crafty.scene(String sceneName, Function init[, Function uninit])
* @param sceneName - Name of the scene to add
* @param init - Function to execute when scene is played
* @param uninit - Function to execute before next scene is played, after entities with `2D` are destroyed
* @sign public void Crafty.scene(String sceneName)
* @param sceneName - Name of scene to play
*
* Method to create scenes on the stage. Pass an ID and function to register a scene.
*
* To play a scene, just pass the ID. When a scene is played, all
* entities with the `2D` component on the stage are destroyed.
*
* If you want some entities to persist over scenes (as in not be destroyed)
* simply add the component `Persist`.
*
* @example
* ~~~
* Crafty.scene("loading", function() {});
*
* Crafty.scene("loading", function() {}, function() {});
*
* Crafty.scene("loading");
* ~~~
*/
scene: function (name, intro, outro) {
//play scene
if (arguments.length === 1) {
Crafty("2D").each(function () {
if (!this.has("Persist")) this.destroy();
});
// uninitialize previous scene
if (this._current !== null && 'uninitialize' in this._scenes[this._current]) {
this._scenes[this._current].uninitialize.call(this);
}
// initialize next scene
this._scenes[name].initialize.call(this);
var oldScene = this._current;
this._current = name;
Crafty.trigger("SceneChange", {
oldScene: oldScene,
newScene: name
});
return;
}
//add scene
this._scenes[name] = {}
this._scenes[name].initialize = intro
if (typeof outro !== 'undefined') {
this._scenes[name].uninitialize = outro;
}
return;
},
/**@
* #Crafty.toRGB
* @category Graphics
* @sign public String Crafty.scene(String hex[, Number alpha])
* @param hex - a 6 character hex number string representing RGB color
* @param alpha - The alpha value.
*
* Get a rgb string or rgba string (if `alpha` presents).
*
* @example
* ~~~
* Crafty.toRGB("ffffff"); // rgb(255,255,255)
* Crafty.toRGB("#ffffff"); // rgb(255,255,255)
* Crafty.toRGB("ffffff", .5); // rgba(255,255,255,0.5)
* ~~~
*
* @see Text.textColor
*/
toRGB: function (hex, alpha) {
var hex = (hex.charAt(0) === '#') ? hex.substr(1) : hex,
c = [], result;
c[0] = parseInt(hex.substr(0, 2), 16);
c[1] = parseInt(hex.substr(2, 2), 16);
c[2] = parseInt(hex.substr(4, 2), 16);
result = alpha === undefined ? 'rgb(' + c.join(',') + ')' : 'rgba(' + c.join(',') + ',' + alpha + ')';
return result;
}
});
/**@
* #Crafty.DrawManager
* @category Graphics
* @sign Crafty.DrawManager
*
* An internal object manage objects to be drawn and implement
* the best method of drawing in both DOM and canvas
*/
Crafty.DrawManager = ( function () {
/** array of dirty rects on screen */
var dirty_rects = [],
/** array of DOMs needed updating */
dom = [];
return {
/**@
* #Crafty.DrawManager.total2D
* @comp Crafty.DrawManager
*
* Total number of the entities that have the `2D` component.
*/
total2D: Crafty("2D").length,
/**@
* #Crafty.DrawManager.onScreen
* @comp Crafty.DrawManager
* @sign public Crafty.DrawManager.onScreen(Object rect)
* @param rect - A rectangle with field {_x: x_val, _y: y_val, _w: w_val, _h: h_val}
*
* Test if a rectangle is completely in viewport
*/
onScreen: function (rect) {
return Crafty.viewport._x + rect._x + rect._w > 0 && Crafty.viewport._y + rect._y + rect._h > 0 &&
Crafty.viewport._x + rect._x < Crafty.viewport.width && Crafty.viewport._y + rect._y < Crafty.viewport.height;
},
/**@
* #Crafty.DrawManager.merge
* @comp Crafty.DrawManager
* @sign public Object Crafty.DrawManager.merge(Object set)
* @param set - an array of rectangular regions
*
* Merged into non overlapping rectangular region
* Its an optimization for the redraw regions.
*/
merge: function (set) {
do {
var newset = [], didMerge = false, i = 0,
l = set.length, current, next, merger;
while (i < l) {
current = set[i];
next = set[i + 1];
if (i < l - 1 && current._x < next._x + next._w && current._x + current._w > next._x &&
current._y < next._y + next._h && current._h + current._y > next._y) {
merger = {
_x: ~~Math.min(current._x, next._x),
_y: ~~Math.min(current._y, next._y),
_w: Math.max(current._x, next._x) + Math.max(current._w, next._w),
_h: Math.max(current._y, next._y) + Math.max(current._h, next._h)
};
merger._w = merger._w - merger._x;
merger._h = merger._h - merger._y;
merger._w = (merger._w == ~~merger._w) ? merger._w : merger._w + 1 | 0;
merger._h = (merger._h == ~~merger._h) ? merger._h : merger._h + 1 | 0;
newset.push(merger);
i++;
didMerge = true;
} else newset.push(current);
i++;
}
set = newset.length ? Crafty.clone(newset) : set;
if (didMerge) i = 0;
} while (didMerge);
return set;
},
/**@
* #Crafty.DrawManager.add
* @comp Crafty.DrawManager
* @sign public Crafty.DrawManager.add(old, current)
* @param old - Undocumented
* @param current - Undocumented
*
* Calculate the bounding rect of dirty data and add to the register of dirty rectangles
*/
add: function add(old, current) {
if (!current) {
dom.push(old);
return;
}
var rect,
before = old._mbr || old,
after = current._mbr || current;
if (old === current) {
rect = old.mbr() || old.pos();
} else {
rect = {
_x: ~~Math.min(before._x, after._x),
_y: ~~Math.min(before._y, after._y),
_w: Math.max(before._w, after._w) + Math.max(before._x, after._x),
_h: Math.max(before._h, after._h) + Math.max(before._y, after._y)
};
rect._w = (rect._w - rect._x);
rect._h = (rect._h - rect._y);
}
if (rect._w === 0 || rect._h === 0 || !this.onScreen(rect)) {
return false;
}
//floor/ceil
rect._x = ~~rect._x;
rect._y = ~~rect._y;
rect._w = (rect._w === ~~rect._w) ? rect._w : rect._w + 1 | 0;
rect._h = (rect._h === ~~rect._h) ? rect._h : rect._h + 1 | 0;
//add to dirty_rects, check for merging
dirty_rects.push(rect);
//if it got merged
return true;
},
/**@
* #Crafty.DrawManager.debug
* @comp Crafty.DrawManager
* @sign public Crafty.DrawManager.debug()
*/
debug: function () {
console.log(dirty_rects, dom);
},
/**@
* #Crafty.DrawManager.draw
* @comp Crafty.DrawManager
* @sign public Crafty.DrawManager.draw([Object rect])
* @param rect - a rectangular region {_x: x_val, _y: y_val, _w: w_val, _h: h_val}
* ~~~
* - If rect is omitted, redraw within the viewport
* - If rect is provided, redraw within the rect
* ~~~
*/
drawAll: function (rect,interpolation) {
var rect = rect || Crafty.viewport.rect(),
inter = interpolation || 0,
q = Crafty.map.search(rect),
i = 0,
l = q.length,
ctx = Crafty.canvas.context,
current;
ctx.clearRect(rect._x, rect._y, rect._w, rect._h);
//sort the objects by the global Z
q.sort(function (a, b) {
return a._globalZ - b._globalZ;
});
for (; i < l; i++) {
current = q[i];
if (current._visible && current.__c.Canvas) {
current.draw();
current._interpolation = inter;
current._changed = false;
}
}
},
/**@
* #Crafty.DrawManager.boundingRect
* @comp Crafty.DrawManager
* @sign public Crafty.DrawManager.boundingRect(set)
* @param set - Undocumented
* ~~~
* - Calculate the common bounding rect of multiple canvas entities.
* - Returns coords
* ~~~
*/
boundingRect: function (set) {
if (!set || !set.length) return;
var newset = [], i = 1,
l = set.length, current, master = set[0], tmp;
master = [master._x, master._y, master._x + master._w, master._y + master._h];
while (i < l) {
current = set[i];
tmp = [current._x, current._y, current._x + current._w, current._y + current._h];
if (tmp[0] < master[0]) master[0] = tmp[0];
if (tmp[1] < master[1]) master[1] = tmp[1];
if (tmp[2] > master[2]) master[2] = tmp[2];
if (tmp[3] > master[3]) master[3] = tmp[3];
i++;
}
tmp = master;
master = {
_x: tmp[0],
_y: tmp[1],
_w: tmp[2] - tmp[0],
_h: tmp[3] - tmp[1]
};
return master;
},
/**@
* #Crafty.DrawManager.draw
* @comp Crafty.DrawManager
* @sign public Crafty.DrawManager.draw()
* ~~~
* - If the number of rects is over 60% of the total number of objects
* do the naive method redrawing `Crafty.DrawManager.drawAll`
* - Otherwise, clear the dirty regions, and redraw entities overlapping the dirty regions.
* ~~~
*
* @see Canvas.draw, DOM.draw
*/
draw: function draw(interpolation) {
//if nothing in dirty_rects, stop
if (!dirty_rects.length && !dom.length) return;
var i = 0, l = dirty_rects.length, k = dom.length, rect, q,
j, len, dupes, obj, ent, objs = [], ctx = Crafty.canvas.context;
//loop over all DOM elements needing updating
for (; i < k; ++i) {
var elem = dom[i];
elem.draw();
elem._changed = false;
elem._interpolation = interpolation;
}
//reset DOM array
dom.length = 0;
//again, stop if nothing in dirty_rects
if (!l) {
return;
}
//if the amount of rects is over 60% of the total objects
//do the naive method redrawing
if (l / this.total2D > 0.6) {
this.drawAll(null,interpolation);
dirty_rects.length = 0;
return;
}
dirty_rects = this.merge(dirty_rects);
for (i = 0; i < l; ++i) { //loop over every dirty rect
rect = dirty_rects[i];
if (!rect) continue;
q = Crafty.map.search(rect, false); //search for ents under dirty rect
dupes = {};
//loop over found objects removing dupes and adding to obj array
for (j = 0, len = q.length; j < len; ++j) {
obj = q[j];
if (dupes[obj[0]] || !obj._visible || !obj.__c.Canvas)
continue;
dupes[obj[0]] = true;
objs.push({
obj: obj,
rect: rect
});
}
//clear the rect from the main canvas
ctx.clearRect(rect._x, rect._y, rect._w, rect._h);
}
//sort the objects by the global Z
objs.sort(function (a, b) {
return a.obj._globalZ - b.obj._globalZ;
});
if (!objs.length){
return;
}
//loop over the objects
for (i = 0, l = objs.length; i < l; ++i) {
obj = objs[i];
rect = obj.rect;
ent = obj.obj;
var area = ent._mbr || ent,
x = (rect._x - area._x <= 0) ? 0 : ~~(rect._x - area._x),
y = (rect._y - area._y < 0) ? 0 : ~~(rect._y - area._y),
w = ~~Math.min(area._w - x, rect._w - (area._x - rect._x), rect._w, area._w),
h = ~~Math.min(area._h - y, rect._h - (area._y - rect._y), rect._h, area._h);
//no point drawing with no width or height
if (h === 0 || w === 0) continue;
ctx.save();
ctx.beginPath();
ctx.moveTo(rect._x, rect._y);
ctx.lineTo(rect._x + rect._w, rect._y);
ctx.lineTo(rect._x + rect._w, rect._h + rect._y);
ctx.lineTo(rect._x, rect._h + rect._y);
ctx.lineTo(rect._x, rect._y);
ctx.clip();
ent._interpolation = interpolation;
ent.draw();
ctx.closePath();
ctx.restore();
//allow entity to re-dirty_rects
ent._changed = false;
}
//empty dirty_rects
dirty_rects.length = 0;
//all merged IDs are now invalid
merged = {};
}
};
})();
Crafty.extend({
/**@
* #Crafty.isometric
* @category 2D
* Place entities in a 45deg isometric fashion.
*/
isometric: {
_tile: {
width: 0,
height: 0
},
_elements:{},
_pos: {
x:0,
y:0
},
_z: 0,
/**@
* #Crafty.isometric.size
* @comp Crafty.isometric
* @sign public this Crafty.isometric.size(Number tileSize)
* @param tileSize - The size of the tiles to place.
*
* Method used to initialize the size of the isometric placement.
* Recommended to use a size alues in the power of `2` (128, 64 or 32).
* This makes it easy to calculate positions and implement zooming.
*
* @example
* ~~~
* var iso = Crafty.isometric.size(128);
* ~~~
*
* @see Crafty.isometric.place
*/
size: function (width, height) {
this._tile.width = width;
this._tile.height = height > 0 ? height : width/2; //Setup width/2 if height doesnt set
return this;
},
/**@
* #Crafty.isometric.place
* @comp Crafty.isometric
* @sign public this Crafty.isometric.size(Number x, Number y, Number z, Entity tile)
* @param x - The `x` position to place the tile
* @param y - The `y` position to place the tile
* @param z - The `z` position or height to place the tile
* @param tile - The entity that should be position in the isometric fashion
*
* Use this method to place an entity in an isometric grid.
*
* @example
* ~~~
* var iso = Crafty.isometric.size(128);
* isos.place(2, 1, 0, Crafty.e('2D, DOM, Color').color('red').attr({w:128, h:128}));
* ~~~
*
* @see Crafty.isometric.size
*/
place: function (x, y, z, obj) {
var pos = this.pos2px(x,y);
pos.top -= z * (this._tile.width / 2);
obj.attr({
x: pos.left + Crafty.viewport._x,
y: pos.top + Crafty.viewport._y
}).z += z;
return this;
},
/**@
* #Crafty.isometric.pos2px
* @comp Crafty.isometric
* @sign public this Crafty.isometric.pos2px(Number x,Number y)
* @param x
* @param y
* @return Object {left Number,top Number}
*
* This method calculate the X and Y Coordiantes to Pixel Positions
*
* @example
* ~~~
* var iso = Crafty.isometric.size(128,96);
* var position = iso.pos2px(100,100); //Object { left=12800, top=4800}
* ~~~
*/
pos2px:function(x,y){
return {
left:x * this._tile.width + (y & 1) * (this._tile.width / 2),
top:y * this._tile.height / 2
}
},
/**@
* #Crafty.isometric.px2pos
* @comp Crafty.isometric
* @sign public this Crafty.isometric.px2pos(Number left,Number top)
* @param top
* @param left
* @return Object {x Number,y Number}
*
* This method calculate pixel top,left positions to x,y coordiantes
*
* @example
* ~~~
* var iso = Crafty.isometric.size(128,96);
* var px = iso.pos2px(12800,4800);
* console.log(px); //Object { x=-100, y=-100}
* ~~~
*/
px2pos:function(left,top){
return {
x:Math.ceil(-left / this._tile.width - (top & 1)*0.5),
y:-top / this._tile.height * 2
};
},
/**@
* #Crafty.isometric.centerAt
* @comp Crafty.isometric
* @sign public this Crafty.isometric.centerAt(Number x,Number y)
* @param top
* @param left
*
* This method center the Viewport at x/y location or gives the current centerpoint of the viewport
*
* @example
* ~~~
* var iso = Crafty.isometric.size(128,96).centerAt(10,10); //Viewport is now moved
* //After moving the viewport by another event you can get the new center point
* console.log(iso.centerAt());
* ~~~
*/
centerAt:function(x,y){
if(typeof x == "number" && typeof y == "number"){
var center = this.pos2px(x,y);
Crafty.viewport._x = -center.left+Crafty.viewport.width/2-this._tile.width/2;
Crafty.viewport._y = -center.top+Crafty.viewport.height/2-this._tile.height/2;
return this;
}else{
return {
top:-Crafty.viewport._y+Crafty.viewport.height/2-this._tile.height/2,
left:-Crafty.viewport._x+Crafty.viewport.width/2-this._tile.width/2
}
}
},
/**@
* #Crafty.isometric.area
* @comp Crafty.isometric
* @sign public this Crafty.isometric.area()
* @return Object {x:{start Number,end Number},y:{start Number,end Number}}
*
* This method get the Area surounding by the centerpoint depends on viewport height and width
*
* @example
* ~~~
* var iso = Crafty.isometric.size(128,96).centerAt(10,10); //Viewport is now moved
* var area = iso.area(); //get the area
* for(var y = area.y.start;y <= area.y.end;y++){
* for(var x = area.x.start ;x <= area.x.end;x++){
* iso.place(x,y,0,Crafty.e("2D,DOM,gras")); //Display tiles in the Screen
* }
* }
* ~~~
*/
area:function(){
//Get the center Point in the viewport
var center = this.centerAt();
var start = this.px2pos(-center.left+Crafty.viewport.width/2,-center.top+Crafty.viewport.height/2);
var end = this.px2pos(-center.left-Crafty.viewport.width/2,-center.top-Crafty.viewport.height/2);
return {
x:{
start : start.x,
end : end.x
},
y:{
start : start.y,
end : end.y
}
};
}
}
}); /**@
* #Particles
* @category Graphics
* Based on Parcycle by Mr. Speaker, licensed under the MIT, Ported by Leo Koppelkamm
* **This is canvas only & won't do anything if the browser doesn't support it!**
* To see how this works take a look in https://github.com/craftyjs/Crafty/blob/master/src/particles.js
*/
Crafty.c("Particles", {
init: function () {
//We need to clone it
this._Particles = Crafty.clone(this._Particles);
},
/**@
* #.particles
* @comp Particles
* @sign public this .particles(Object options)
* @param options - Map of options that specify the behavior and look of the particles.
*
* @example
* ~~~
* var options = {
* maxParticles: 150,
* size: 18,
* sizeRandom: 4,
* speed: 1,
* speedRandom: 1.2,
* // Lifespan in frames
* lifeSpan: 29,
* lifeSpanRandom: 7,
* // Angle is calculated clockwise: 12pm is 0deg, 3pm is 90deg etc.
* angle: 65,
* angleRandom: 34,
* startColour: [255, 131, 0, 1],
* startColourRandom: [48, 50, 45, 0],
* endColour: [245, 35, 0, 0],
* endColourRandom: [60, 60, 60, 0],
* // Only applies when fastMode is off, specifies how sharp the gradients are drawn
* sharpness: 20,
* sharpnessRandom: 10,
* // Random spread from origin
* spread: 10,
* // How many frames should this last
* duration: -1,
* // Will draw squares instead of circle gradients
* fastMode: false,
* gravity: { x: 0, y: 0.1 },
* // sensible values are 0-3
* jitter: 0
* }
*
* Crafty.e("2D,Canvas,Particles").particles(options);
* ~~~
*/
particles: function (options) {
if (!Crafty.support.canvas || Crafty.deactivateParticles) return this;
//If we drew on the main canvas, we'd have to redraw
//potentially huge sections of the screen every frame
//So we create a separate canvas, where we only have to redraw
//the changed particles.
var c, ctx, relativeX, relativeY, bounding;
c = document.createElement("canvas");
c.width = Crafty.viewport.width;
c.height = Crafty.viewport.height;
c.style.position = 'absolute';
Crafty.stage.elem.appendChild(c);
ctx = c.getContext('2d');
this._Particles.init(options);
// Clean up the DOM when this component is removed
this.bind('Remove', function () {
Crafty.stage.elem.removeChild(c);
}).bind("RemoveComponent", function (id) {
if (id === "particles")
Crafty.stage.elem.removeChild(c);
});
;
relativeX = this.x + Crafty.viewport.x;
relativeY = this.y + Crafty.viewport.y;
this._Particles.position = this._Particles.vectorHelpers.create(relativeX, relativeY);
var oldViewport = {
x: Crafty.viewport.x,
y: Crafty.viewport.y
};
this.bind('EnterFrame', function () {
relativeX = this.x + Crafty.viewport.x;
relativeY = this.y + Crafty.viewport.y;
this._Particles.viewportDelta = {
x: Crafty.viewport.x - oldViewport.x,
y: Crafty.viewport.y - oldViewport.y
};
oldViewport = {
x: Crafty.viewport.x,
y: Crafty.viewport.y
};
this._Particles.position = this._Particles.vectorHelpers.create(relativeX, relativeY);
//Selective clearing
if (typeof Crafty.DrawManager.boundingRect == 'function') {
bounding = Crafty.DrawManager.boundingRect(this._Particles.register);
if (bounding) ctx.clearRect(bounding._x, bounding._y, bounding._w, bounding._h);
} else {
ctx.clearRect(0, 0, Crafty.viewport.width, Crafty.viewport.height);
}
//This updates all particle colors & positions
this._Particles.update();
//This renders the updated particles
this._Particles.render(ctx);
});
return this;
},
_Particles: {
presets: {
maxParticles: 150,
size: 18,
sizeRandom: 4,
speed: 1,
speedRandom: 1.2,
// Lifespan in frames
lifeSpan: 29,
lifeSpanRandom: 7,
// Angle is calculated clockwise: 12pm is 0deg, 3pm is 90deg etc.
angle: 65,
angleRandom: 34,
startColour: [255, 131, 0, 1],
startColourRandom: [48, 50, 45, 0],
endColour: [245, 35, 0, 0],
endColourRandom: [60, 60, 60, 0],
// Only applies when fastMode is off, specifies how sharp the gradients are drawn
sharpness: 20,
sharpnessRandom: 10,
// Random spread from origin
spread: 10,
// How many frames should this last
duration: -1,
// Will draw squares instead of circle gradients
fastMode: false,
gravity: {
x: 0,
y: 0.1
},
// sensible values are 0-3
jitter: 0,
//Don't modify the following
particles: [],
active: true,
particleCount: 0,
elapsedFrames: 0,
emissionRate: 0,
emitCounter: 0,
particleIndex: 0
},
init: function (options) {
this.position = this.vectorHelpers.create(0, 0);
if (typeof options == 'undefined') var options = {};
//Create current config by mergin given options and presets.
for (key in this.presets) {
if (typeof options[key] != 'undefined') this[key] = options[key];
else this[key] = this.presets[key];
}
this.emissionRate = this.maxParticles / this.lifeSpan;
this.positionRandom = this.vectorHelpers.create(this.spread, this.spread);
},
addParticle: function () {
if (this.particleCount == this.maxParticles) {
return false;
}
// Take the next particle out of the particle pool we have created and initialize it
var particle = new this.particle(this.vectorHelpers);
this.initParticle(particle);
this.particles[this.particleCount] = particle;
// Increment the particle count
this.particleCount++;
return true;
},
RANDM1TO1: function () {
return Math.random() * 2 - 1;
},
initParticle: function (particle) {
particle.position.x = this.position.x + this.positionRandom.x * this.RANDM1TO1();
particle.position.y = this.position.y + this.positionRandom.y * this.RANDM1TO1();
var newAngle = (this.angle + this.angleRandom * this.RANDM1TO1()) * (Math.PI / 180); // convert to radians
var vector = this.vectorHelpers.create(Math.sin(newAngle), -Math.cos(newAngle)); // Could move to lookup for speed
var vectorSpeed = this.speed + this.speedRandom * this.RANDM1TO1();
particle.direction = this.vectorHelpers.multiply(vector, vectorSpeed);
particle.size = this.size + this.sizeRandom * this.RANDM1TO1();
particle.size = particle.size < 0 ? 0 : ~~particle.size;
particle.timeToLive = this.lifeSpan + this.lifeSpanRandom * this.RANDM1TO1();
particle.sharpness = this.sharpness + this.sharpnessRandom * this.RANDM1TO1();
particle.sharpness = particle.sharpness > 100 ? 100 : particle.sharpness < 0 ? 0 : particle.sharpness;
// internal circle gradient size - affects the sharpness of the radial gradient
particle.sizeSmall = ~~((particle.size / 200) * particle.sharpness); //(size/2/100)
var start = [
this.startColour[0] + this.startColourRandom[0] * this.RANDM1TO1(),
this.startColour[1] + this.startColourRandom[1] * this.RANDM1TO1(),
this.startColour[2] + this.startColourRandom[2] * this.RANDM1TO1(),
this.startColour[3] + this.startColourRandom[3] * this.RANDM1TO1()
];
var end = [
this.endColour[0] + this.endColourRandom[0] * this.RANDM1TO1(),
this.endColour[1] + this.endColourRandom[1] * this.RANDM1TO1(),
this.endColour[2] + this.endColourRandom[2] * this.RANDM1TO1(),
this.endColour[3] + this.endColourRandom[3] * this.RANDM1TO1()
];
particle.colour = start;
particle.deltaColour[0] = (end[0] - start[0]) / particle.timeToLive;
particle.deltaColour[1] = (end[1] - start[1]) / particle.timeToLive;
particle.deltaColour[2] = (end[2] - start[2]) / particle.timeToLive;
particle.deltaColour[3] = (end[3] - start[3]) / particle.timeToLive;
},
update: function () {
if (this.active && this.emissionRate > 0) {
var rate = 1 / this.emissionRate;
this.emitCounter++;
while (this.particleCount < this.maxParticles && this.emitCounter > rate) {
this.addParticle();
this.emitCounter -= rate;
}
this.elapsedFrames++;
if (this.duration != -1 && this.duration < this.elapsedFrames) {
this.stop();
}
}
this.particleIndex = 0;
this.register = [];
var draw;
while (this.particleIndex < this.particleCount) {
var currentParticle = this.particles[this.particleIndex];
// If the current particle is alive then update it
if (currentParticle.timeToLive > 0) {
// Calculate the new direction based on gravity
currentParticle.direction = this.vectorHelpers.add(currentParticle.direction, this.gravity);
currentParticle.position = this.vectorHelpers.add(currentParticle.position, currentParticle.direction);
currentParticle.position = this.vectorHelpers.add(currentParticle.position, this.viewportDelta);
if (this.jitter) {
currentParticle.position.x += this.jitter * this.RANDM1TO1();
currentParticle.position.y += this.jitter * this.RANDM1TO1();
}
currentParticle.timeToLive--;
// Update colours
var r = currentParticle.colour[0] += currentParticle.deltaColour[0];
var g = currentParticle.colour[1] += currentParticle.deltaColour[1];
var b = currentParticle.colour[2] += currentParticle.deltaColour[2];
var a = currentParticle.colour[3] += currentParticle.deltaColour[3];
// Calculate the rgba string to draw.
draw = [];
draw.push("rgba(" + (r > 255 ? 255 : r < 0 ? 0 : ~~r));
draw.push(g > 255 ? 255 : g < 0 ? 0 : ~~g);
draw.push(b > 255 ? 255 : b < 0 ? 0 : ~~b);
draw.push((a > 1 ? 1 : a < 0 ? 0 : a.toFixed(2)) + ")");
currentParticle.drawColour = draw.join(",");
if (!this.fastMode) {
draw[3] = "0)";
currentParticle.drawColourEnd = draw.join(",");
}
this.particleIndex++;
} else {
// Replace particle with the last active
if (this.particleIndex != this.particleCount - 1) {
this.particles[this.particleIndex] = this.particles[this.particleCount - 1];
}
this.particleCount--;
}
var rect = {};
rect._x = ~~currentParticle.position.x;
rect._y = ~~currentParticle.position.y;
rect._w = currentParticle.size;
rect._h = currentParticle.size;
this.register.push(rect);
}
},
stop: function () {
this.active = false;
this.elapsedFrames = 0;
this.emitCounter = 0;
},
render: function (context) {
for (var i = 0, j = this.particleCount; i < j; i++) {
var particle = this.particles[i];
var size = particle.size;
var halfSize = size >> 1;
if (particle.position.x + size < 0
|| particle.position.y + size < 0
|| particle.position.x - size > Crafty.viewport.width
|| particle.position.y - size > Crafty.viewport.height) {
//Particle is outside
continue;
}
var x = ~~particle.position.x;
var y = ~~particle.position.y;
if (this.fastMode) {
context.fillStyle = particle.drawColour;
} else {
var radgrad = context.createRadialGradient(x + halfSize, y + halfSize, particle.sizeSmall, x + halfSize, y + halfSize, halfSize);
radgrad.addColorStop(0, particle.drawColour);
//0.9 to avoid visible boxing
radgrad.addColorStop(0.9, particle.drawColourEnd);
context.fillStyle = radgrad;
}
context.fillRect(x, y, size, size);
}
},
particle: function (vectorHelpers) {
this.position = vectorHelpers.create(0, 0);
this.direction = vectorHelpers.create(0, 0);
this.size = 0;
this.sizeSmall = 0;
this.timeToLive = 0;
this.colour = [];
this.drawColour = "";
this.deltaColour = [];
this.sharpness = 0;
},
vectorHelpers: {
create: function (x, y) {
return {
"x": x,
"y": y
};
},
multiply: function (vector, scaleFactor) {
vector.x *= scaleFactor;
vector.y *= scaleFactor;
return vector;
},
add: function (vector1, vector2) {
vector1.x += vector2.x;
vector1.y += vector2.y;
return vector1;
}
}
}
});
Crafty.extend({
/**@
* #Crafty.audio
* @category Audio
*
* Add sound files and play them. Chooses best format for browser support.
* Due to the nature of HTML5 audio, three types of audio files will be
* required for cross-browser capabilities. These formats are MP3, Ogg and WAV.
* When sound was not muted on before pause, sound will be unmuted after unpause.
* When sound is muted Crafty.pause() does not have any effect on sound.
*/
audio:{
sounds:{},
supported:{},
codecs :{ // Chart from jPlayer
ogg: 'audio/ogg; codecs="vorbis"', //OGG
wav: 'audio/wav; codecs="1"', // PCM
webma: 'audio/webm; codecs="vorbis"',// WEBM
mp3: 'audio/mpeg; codecs="mp3"', //MP3
m4a: 'audio/mp4; codecs="mp4a.40.2"'// AAC / MP4
},
volume:1, //Global Volume
muted:false,
/**
* Function to setup supported formats
**/
canPlay:function(){
var audio = this.audioElement(),canplay;
for(var i in this.codecs){
canplay = audio.canPlayType(this.codecs[i]);
if(canplay !== "" && canplay !== "no"){
this.supported[i] = true;
}else{
this.supported[i] = false;
}
}
},
/**
* Function to get an Audio Element
**/
audioElement:function(){
//IE does not support Audio Object
return typeof Audio !== 'undefined' ? new Audio("") : document.createElement('audio');
},
/**@
* #Crafty.audio.add
* @comp Crafty.audio
* @sign public this Crafty.audio.add(String id, String url)
* @param id - A string to reffer to sounds
* @param url - A string pointing to the sound file
* @sign public this Crafty.audio.add(String id, Array urls)
* @param urls - Array of urls pointing to different format of the same sound, selecting the first that is playable
* @sign public this Crafty.audio.add(Object map)
* @param map - key-value pairs where the key is the `id` and the value is either a `url` or `urls`
*
* Loads a sound to be played. Due to the nature of HTML5 audio,
* three types of audio files will be required for cross-browser capabilities.
* These formats are MP3, Ogg and WAV.
*
* Passing an array of URLs will determine which format the browser can play and select it over any other.
*
* Accepts an object where the key is the audio name and
* either a URL or an Array of URLs (to determine which type to use).
*
* The ID you use will be how you refer to that sound when using `Crafty.audio.play`.
*
* @example
* ~~~
* //adding audio from an object
* Crafty.audio.add({
* shoot: ["sounds/shoot.wav",
* "sounds/shoot.mp3",
* "sounds/shoot.ogg"],
*
* coin: "sounds/coin.mp3"
* });
*
* //adding a single sound
* Crafty.audio.add("walk", [
* "sounds/walk.mp3",
* "sounds/walk.ogg",
* "sounds/walk.wav"
* ]);
*
* //only one format
* Crafty.audio.add("jump", "sounds/jump.mp3");
* ~~~
*/
add:function(id,url){
Crafty.support.audio = !!this.audioElement().canPlayType; //Setup audio support
if (!Crafty.support.audio) return;
this.canPlay(); //Setup supported Extensions
var audio,ext,path;
if(arguments.length === 1 && typeof id === "object"){
for(var i in id){
for(var src in id[i]){
audio = this.audioElement();
audio.id = i;
audio.preload = "auto";
audio.volume = Crafty.audio.volume;
path = id[i][src];
ext = path.substr(path.lastIndexOf('.') + 1).toLowerCase();
if(this.supported[ext]){
audio.src = path;
Crafty.asset(path, audio);
this.sounds[i] = {
obj:audio,
played:0
}
}
}
}
}
if(typeof id === "string"){
audio = this.audioElement();
audio.id = id;
audio.preload = "auto";
audio.volume = Crafty.audio.volume;
if(typeof url === "string"){
ext = url.substr(url.lastIndexOf('.') + 1).toLowerCase();
if(this.supported[ext]){
audio.src = url;
Crafty.asset(url, audio);
this.sounds[id] = {
obj:audio,
played:0
}
}
}
if(typeof url === "object"){
for(src in url){
audio = this.audioElement();
audio.id = id;
audio.preload = "auto";
audio.volume = Crafty.audio.volume;
path = url[src];
ext = path.substr(path.lastIndexOf('.') + 1).toLowerCase();
if(this.supported[ext]){
audio.src = path;
Crafty.asset(path, audio);
this.sounds[id] = {
obj:audio,
played:0
}
}
}
}
}
},
/**@
* #Crafty.audio.play
* @comp Crafty.audio
* @sign public this Crafty.audio.play(String id)
* @sign public this Crafty.audio.play(String id, Number repeatCount)
* @sign public this Crafty.audio.play(String id, Number repeatCount,Number volume)
* @param id - A string to reffer to sounds
* @param repeatCount - Repeat count for the file, where -1 stands for repeat forever.
* @param volume - volume can be a number between 0.0 and 1.0
*
* Will play a sound previously added by using the ID that was used in `Crafty.audio.add`.
* Has a default maximum of 5 channels so that the same sound can play simultaneously unless all of the channels are playing.
* *Note that the implementation of HTML5 Audio is buggy at best.*
*
* @example
* ~~~
* Crafty.audio.play("walk");
*
* //play and repeat forever
* Crafty.audio.play("backgroundMusic", -1);
* Crafty.audio.play("explosion",1,0.5); //play sound once with volume of 50%
* ~~~
*/
play:function(id,repeat,volume){
if(repeat == 0 || !Crafty.support.audio || !this.sounds[id]) return;
var s = this.sounds[id];
s.obj.volume = volume || Crafty.audio.volume ;
if(s.obj.currentTime) s.obj.currentTime = 0;
s.obj.play();
s.played ++;
s.obj.addEventListener("ended", function(){
if(s.played < repeat || repeat == -1){
if(this.currentTime) this.currentTime = 0;
this.play();
s.played ++;
}
},true);
},
/**@
* #Crafty.audio.stop
* @sign public this Crafty.audio.stop([Number ID])
*
* Stops any playnig sound. if id is not set, stop all sounds which are playing
*
* @example
* ~~~
* //all sounds stopped playing now
* Crafty.audio.stop();
*
* ~~~
*/
stop:function(id){
if(!Crafty.support.audio) return;
var s;
if(!id){
for(var i in this.sounds){
s = this.sounds[i];
if(!s.obj.paused) s.obj.pause();
}
}
if(!this.sounds[id]) return;
s = this.sounds[id];
if(!s.obj.paused) s.obj.pause();
},
/**@
* #Crafty.audio.mute
* @sign public this Crafty.audio.mute([Boolean mute])
*
* Mute or unmute every Audio instance that is playing. Toggles between
* pausing or playing depending on the state.
*
* @example
* ~~~
* //toggle mute and unmute depending on current state
* Crafty.audio.mute();
* ~~~
*/
mute:function(){
if(!Crafty.support.audio) return;
var s;
if(!this.muted){
for(var i in this.sounds){
s = this.sounds[i];
s.obj.pause();
}
this.muted = true;
}else{
for(var i in this.sounds){
s = this.sounds[i];
if(s.obj.currentTime && s.obj.currentTime > 0)
this.sounds[i].obj.play();
}
this.muted = false;
}
}
}
}); /**@
* #Text
* @category Graphics
* @trigger Change - when the text is changed
* @requires Canvas or DOM
* Component to draw text inside the body of an entity.
*/
Crafty.c("Text", {
_text: "",
_textFont: {
"type": "",
"weight": "",
"size": "",
"family": ""
},
ready: true,
init: function () {
this.requires("2D");
this.bind("Draw", function (e) {
var font = this._textFont["type"] + ' ' + this._textFont["weight"] + ' ' +
this._textFont["size"] + ' ' + this._textFont["family"];
if (e.type === "DOM") {
var el = this._element,
style = el.style;
style.color = this._textColor;
style.font = font;
el.innerHTML = this._text;
} else if (e.type === "canvas") {
var context = e.ctx,
metrics = null;
context.save();
context.fillStyle = this._textColor || "rgb(0,0,0)";
context.font = font;
context.translate(this.x, this.y + this.h);
context.fillText(this._text, 0, 0);
metrics = context.measureText(this._text);
this._w = metrics.width;
context.restore();
}
});
},
/**@
* #.text
* @comp Text
* @sign public this .text(String text)
* @sign public this .text(Function textgenerator)
* @param text - String of text that will be inserted into the DOM or Canvas element.
*
* This method will update the text inside the entity.
* If you use DOM, to modify the font, use the `.css` method inherited from the DOM component.
*
* If you need to reference attributes on the entity itself you can pass a function instead of a string.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 }).text("Look at me!!");
*
* Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 })
* .text(function () { return "My position is " + this._x });
*
* Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 }).text("Look at me!!");
*
* Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 })
* .text(function () { return "My position is " + this._x });
* ~~~
*/
text: function (text) {
if (!text) return this._text;
if (typeof(text) == "function")
this._text = text.call(this);
else
this._text = text;
this.trigger("Change");
return this;
},
/**@
* #.textColor
* @comp Text
* @sign public this .textColor(String color, Number strength)
* @param color - The color in hexidecimal
* @param strength - Level of opacity
*
* Modify the text color and level of opacity.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 }).text("Look at me!!")
* .textColor('#FF0000');
*
* Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 }).text('Look at me!!')
* .textColor('#FF0000', 0.6);
* ~~~
* @see Crafty.toRGB
*/
textColor: function (color, strength) {
this._strength = strength;
this._textColor = Crafty.toRGB(color, this._strength);
this.trigger("Change");
return this;
},
/**@
* #.textFont
* @comp Text
* @triggers Change
* @sign public this .textFont(String key, * value)
* @param key - Property of the entity to modify
* @param value - Value to set the property to
*
* @sign public this .textFont(Object map)
* @param map - Object where the key is the property to modify and the value as the property value
*
* Use this method to set font property of the text entity.
*
* @example
* ~~~
* Crafty.e("2D, DOM, Text").textFont({ type: 'italic', family: 'Arial' });
* Crafty.e("2D, Canvas, Text").textFont({ size: '20px', weight: 'bold' });
*
* Crafty.e("2D, Canvas, Text").textFont("type", "italic");
* Crafty.e("2D, Canvas, Text").textFont("type"); // italic
* ~~~
*/
textFont: function (key, value) {
if (arguments.length === 1) {
//if just the key, return the value
if (typeof key === "string") {
return this._textFont[key];
}
if (typeof key === "object") {
for (propertyKey in key) {
this._textFont[propertyKey] = key[propertyKey];
}
}
} else {
this._textFont[key] = value;
}
this.trigger("Change");
return this;
}
});
Crafty.extend({
/**@
* #Crafty.assets
* @category Assets
* An object containing every asset used in the current Crafty game.
* The key is the URL and the value is the `Audio` or `Image` object.
*
* If loading an asset, check that it is in this object first to avoid loading twice.
*
* @example
* ~~~
* var isLoaded = !!Crafty.assets["images/sprite.png"];
* ~~~
* @see Crafty.loader
*/
assets: {},
/**@
* #Crafty.asset
* @category Assets
*
* @trigger NewAsset - After setting new asset - Object - key and value of new added asset.
* @sign public void Crafty.asset(String key, Object asset)
* @param key - asset url.
* @param asset - Audio` or `Image` object.
* Add new asset to assets object.
*
* @sign public void Crafty.asset(String key)
* @param key - asset url.
* Get asset from assets object.
*
* @example
* ~~~
* Crafty.asset(key, value);
* var asset = Crafty.asset(key); //object with key and value fields
* ~~~
*
* @see Crafty.assets
*/
asset: function(key, value) {
if (arguments.length === 1) {
return Crafty.assets[key];
}
if (!Crafty.assets[key]) {
Crafty.assets[key] = value;
this.trigger("NewAsset", {
key : key,
value : value
});
}
},
/**@
* #Crafty.loader
* @category Assets
* @sign public void Crafty.load(Array assets, Function onLoad[, Function onProgress, Function onError])
* @param assets - Array of assets to load (accepts sounds and images)
* @param onLoad - Callback when the assets are loaded
* @param onProgress - Callback when an asset is loaded. Contains information about assets loaded
* @param onError - Callback when an asset fails to load
*
* Preloader for all assets. Takes an array of URLs and
* adds them to the `Crafty.assets` object.
*
* Files with suffixes `jpg`, `jpeg`, `gif` and `png` (case insensitive) will be loaded.
*
* If `Crafty.support.audio` is `true`, files with the following suffixes `mp3`, `wav`, `ogg` and `mp4` (case insensitive) can be loaded.
*
* The `onProgress` function will be passed on object with information about
* the progress including how many assets loaded, total of all the assets to
* load and a percentage of the progress.
* ~~~
* { loaded: j, total: total, percent: (j / total * 100) ,src:src})
* ~~~
*
* `onError` will be passed with the asset that couldn't load.
*
* When `onError` is not provided, the onLoad is loaded even some assests are not successfully loaded. Otherwise, onLoad will be called no matter whether there are errors or not.
*
* @example
* ~~~
* Crafty.load(["images/sprite.png", "sounds/jump.mp3"],
* function() {
* //when loaded
* Crafty.scene("main"); //go to main scene
* Crafty.audio.play("jump.mp3"); //Play the audio file
* },
*
* function(e) {
* //progress
* },
*
* function(e) {
* //uh oh, error loading
* }
* );
* ~~~
*
* @see Crafty.assets
*/
load: function (data, oncomplete, onprogress, onerror) {
var i = 0, l = data.length, current, obj, total = l, j = 0, ext = "" ;
//Progress function
function pro(){
var src = this.src;
//Remove events cause audio trigger this event more than once(depends on browser)
if (this.removeEventListener) {
this.removeEventListener('canplaythrough', pro, false);
}
++j;
//if progress callback, give information of assets loaded, total and percent
if (onprogress)
onprogress({
loaded: j,
total: total,
percent: (j / total * 100),
src:src
});
if(j === total && oncomplete) oncomplete();
};
//Error function
function err(){
var src = this.src;
if (onerror)
onerror({
loaded: j,
total: total,
percent: (j / total * 100),
src:src
});
j++;
if(j === total && oncomplete) oncomplete();
};
for (; i < l; ++i) {
current = data[i];
ext = current.substr(current.lastIndexOf('.') + 1).toLowerCase();
obj = Crafty.asset(current) || null;
if (Crafty.support.audio && Crafty.audio.supported[ext]) {
//Create new object if not exists
if(!obj){
var name = current.substr(current.lastIndexOf('/') + 1).toLowerCase();
obj = Crafty.audio.audioElement();
obj.id = name;
obj.src = current;
obj.preload = "auto";
obj.volume = Crafty.audio.volume;
Crafty.asset(current, obj);
Crafty.audio.sounds[name] = {
obj:obj,
played:0
}
}
//addEventListener is supported on IE9 , Audio as well
if (obj.addEventListener) {
obj.addEventListener('canplaythrough', pro, false);
}
} else if (ext === "jpg" || ext === "jpeg" || ext === "gif" || ext === "png") {
if(!obj) {
obj = new Image();
Crafty.asset(current, obj);
}
obj.onload=pro;
obj.src = current; //setup src after onload function Opera/IE Bug
} else {
total--;
continue; //skip if not applicable
}
obj.onerror = err;
}
},
/**@
* #Crafty.modules
* @category Assets
* @sign public void Crafty.modules([String repoLocation,] Object moduleMap[, Function onLoad])
* @param modules - Map of name:version pairs for modules to load
* @param onLoad - Callback when the modules are loaded
*
* Browse the selection of modules on crafty repositories.
* Downloads and executes the javascript in the specified modules.
* If no repository is specified it defaults to http://cdn.craftycomponents.com
*
* Available repositories:
*
* - http://cdn.craftycomponents.com
* - http://cdn.crafty-modules.com
*
*
* @example
* ~~~
* // Loading from default repository
* Crafty.modules({ moveto: 'DEV' }, function () {
* //module is ready
* Crafty.e("MoveTo, 2D, DOM");
* });
*
* // Loading from your own server
* Crafty.modules({ 'http://mydomain.com/js/mystuff.js': 'DEV' }, function () {
* //module is ready
* Crafty.e("MoveTo, 2D, DOM");
* });
*
* // Loading from alternative repository
* Crafty.modules('http://cdn.crafty-modules.com', { moveto: 'DEV' }, function () {
* //module is ready
* Crafty.e("MoveTo, 2D, DOM");
* });
*
* // Loading from the latest component website
* Crafty.modules(
* 'http://cdn.craftycomponents.com'
* , { MoveTo: 'release' }
* , function () {
* Crafty.e("2D, DOM, Color, MoveTo")
* .attr({x: 0, y: 0, w: 50, h: 50})
* .color("green");
* });
* });
* ~~~
*
*/
modules: function (modulesRepository, moduleMap, oncomplete) {
if (arguments.length === 2 && typeof modulesRepository === "object") {
oncomplete = moduleMap;
moduleMap = modulesRepository;
modulesRepository = 'http://cdn.craftycomponents.com';
}
/*!
* $script.js Async loader & dependency manager
* https://github.com/ded/script.js
* (c) Dustin Diaz, Jacob Thornton 2011
* License: MIT
*/
var $script = (function () {
var win = this, doc = document
, head = doc.getElementsByTagName('head')[0]
, validBase = /^https?:\/\//
, old = win.$script, list = {}, ids = {}, delay = {}, scriptpath
, scripts = {}, s = 'string', f = false
, push = 'push', domContentLoaded = 'DOMContentLoaded', readyState = 'readyState'
, addEventListener = 'addEventListener', onreadystatechange = 'onreadystatechange'
function every(ar, fn, i) {
for (i = 0, j = ar.length; i < j; ++i) if (!fn(ar[i])) return f
return 1
}
function each(ar, fn) {
every(ar, function (el) {
return !fn(el)
})
}
if (!doc[readyState] && doc[addEventListener]) {
doc[addEventListener](domContentLoaded, function fn() {
doc.removeEventListener(domContentLoaded, fn, f)
doc[readyState] = 'complete'
}, f)
doc[readyState] = 'loading'
}
function $script(paths, idOrDone, optDone) {
paths = paths[push] ? paths : [paths]
var idOrDoneIsDone = idOrDone && idOrDone.call
, done = idOrDoneIsDone ? idOrDone : optDone
, id = idOrDoneIsDone ? paths.join('') : idOrDone
, queue = paths.length
function loopFn(item) {
return item.call ? item() : list[item]
}
function callback() {
if (!--queue) {
list[id] = 1
done && done()
for (var dset in delay) {
every(dset.split('|'), loopFn) && !each(delay[dset], loopFn) && (delay[dset] = [])
}
}
}
setTimeout(function () {
each(paths, function (path) {
if (scripts[path]) {
id && (ids[id] = 1)
return scripts[path] == 2 && callback()
}
scripts[path] = 1
id && (ids[id] = 1)
create(!validBase.test(path) && scriptpath ? scriptpath + path + '.js' : path, callback)
})
}, 0)
return $script
}
function create(path, fn) {
var el = doc.createElement('script')
, loaded = f
el.onload = el.onerror = el[onreadystatechange] = function () {
if ((el[readyState] && !(/^c|loade/.test(el[readyState]))) || loaded) return;
el.onload = el[onreadystatechange] = null
loaded = 1
scripts[path] = 2
fn()
}
el.async = 1
el.src = path
head.insertBefore(el, head.firstChild)
}
$script.get = create
$script.order = function (scripts, id, done) {
(function callback(s) {
s = scripts.shift()
if (!scripts.length) $script(s, id, done)
else $script(s, callback)
}())
}
$script.path = function (p) {
scriptpath = p
}
$script.ready = function (deps, ready, req) {
deps = deps[push] ? deps : [deps]
var missing = [];
!each(deps, function (dep) {
list[dep] || missing[push](dep);
}) && every(deps, function (dep) {
return list[dep]
}) ?
ready() : !function (key) {
delay[key] = delay[key] || []
delay[key][push](ready)
req && req(missing)
}(deps.join('|'))
return $script
}
$script.noConflict = function () {
win.$script = old;
return this
}
return $script
})();
var modules = [];
var validBase = /^(https?|file):\/\//;
for (var i in moduleMap) {
if (validBase.test(i))
modules.push(i)
else
modules.push(modulesRepository + '/' + i.toLowerCase() + '-' + moduleMap[i].toLowerCase() + '.js');
}
$script(modules, function () {
if (oncomplete) oncomplete();
});
}
});
/**@
* #Crafty.math
* @category 2D
* Static functions.
*/
Crafty.math = {
/**@
* #Crafty.math.abs
* @comp Crafty.math
* @sign public this Crafty.math.abs(Number n)
* @param n - Some value.
* @return Absolute value.
*
* Returns the absolute value.
*/
abs: function (x) {
return x < 0 ? -x : x;
},
/**@
* #Crafty.math.amountOf
* @comp Crafty.math
* @sign public Number Crafty.math.amountOf(Number checkValue, Number minValue, Number maxValue)
* @param checkValue - Value that should checked with minimum and maximum.
* @param minValue - Minimum value to check.
* @param maxValue - Maximum value to check.
* @return Amount of checkValue compared to minValue and maxValue.
*
* Returns the amount of how much a checkValue is more like minValue (=0)
* or more like maxValue (=1)
*/
amountOf: function (checkValue, minValue, maxValue) {
if (minValue < maxValue)
return (checkValue - minValue) / (maxValue - minValue);
else
return (checkValue - maxValue) / (minValue - maxValue);
},
/**@
* #Crafty.math.clamp
* @comp Crafty.math
* @sign public Number Crafty.math.clamp(Number value, Number min, Number max)
* @param value - A value.
* @param max - Maximum that value can be.
* @param min - Minimum that value can be.
* @return The value between minimum and maximum.
*
* Restricts a value to be within a specified range.
*/
clamp: function (value, min, max) {
if (value > max)
return max;
else if (value < min)
return min;
else
return value;
},
/**@
* Converts angle from degree to radian.
* @comp Crafty.math
* @param angleInDeg - The angle in degree.
* @return The angle in radian.
*/
degToRad: function (angleInDeg) {
return angleInDeg * Math.PI / 180;
},
/**@
* #Crafty.math.distance
* @comp Crafty.math
* @sign public Number Crafty.math.distance(Number x1, Number y1, Number x2, Number y2)
* @param x1 - First x coordinate.
* @param y1 - First y coordinate.
* @param x2 - Second x coordinate.
* @param y2 - Second y coordinate.
* @return The distance between the two points.
*
* Distance between two points.
*/
distance: function (x1, y1, x2, y2) {
var squaredDistance = Crafty.math.squaredDistance(x1, y1, x2, y2);
return Math.sqrt(parseFloat(squaredDistance));
},
/**@
* #Crafty.math.lerp
* @comp Crafty.math
* @sign public Number Crafty.math.lerp(Number value1, Number value2, Number amount)
* @param value1 - One value.
* @param value2 - Another value.
* @param amount - Amount of value2 to value1.
* @return Linear interpolated value.
*
* Linear interpolation. Passing amount with a value of 0 will cause value1 to be returned,
* a value of 1 will cause value2 to be returned.
*/
lerp: function (value1, value2, amount) {
return value1 + (value2 - value1) * amount;
},
/**@
* #Crafty.math.negate
* @comp Crafty.math
* @sign public Number Crafty.math.negate(Number percent)
* @param percent - If you pass 1 a -1 will be returned. If you pass 0 a 1 will be returned.
* @return 1 or -1.
*
* Returnes "randomly" -1.
*/
negate: function (percent) {
if (Math.random() < percent)
return -1;
else
return 1;
},
/**@
* #Crafty.math.radToDeg
* @comp Crafty.math
* @sign public Number Crafty.math.radToDeg(Number angle)
* @param angleInRad - The angle in radian.
* @return The angle in degree.
*
* Converts angle from radian to degree.
*/
radToDeg: function (angleInRad) {
return angleInRad * 180 / Math.PI;
},
/**@
* #Crafty.math.randomElementOfArray
* @comp Crafty.math
* @sign public Object Crafty.math.randomElementOfArray(Array array)
* @param array - A specific array.
* @return A random element of a specific array.
*
* Returns a random element of a specific array.
*/
randomElementOfArray: function (array) {
return array[Math.floor(array.length * Math.random())];
},
/**@
* #Crafty.math.randomInt
* @comp Crafty.math
* @sign public Number Crafty.math.randomInt(Number start, Number end)
* @param start - Smallest int value that can be returned.
* @param end - Biggest int value that can be returned.
* @return A random int.
*
* Returns a random int in within a specific range.
*/
randomInt: function (start, end) {
return start + Math.floor((1 + end - start) * Math.random());
},
/**@
* #Crafty.math.randomNumber
* @comp Crafty.math
* @sign public Number Crafty.math.randomInt(Number start, Number end)
* @param start - Smallest number value that can be returned.
* @param end - Biggest number value that can be returned.
* @return A random number.
*
* Returns a random number in within a specific range.
*/
randomNumber: function (start, end) {
return start + (end - start) * Math.random();
},
/**@
* #Crafty.math.squaredDistance
* @comp Crafty.math
* @sign public Number Crafty.math.squaredDistance(Number x1, Number y1, Number x2, Number y2)
* @param x1 - First x coordinate.
* @param y1 - First y coordinate.
* @param x2 - Second x coordinate.
* @param y2 - Second y coordinate.
* @return The squared distance between the two points.
*
* Squared distance between two points.
*/
squaredDistance: function (x1, y1, x2, y2) {
return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
},
/**@
* #Crafty.math.squaredDistance
* @comp Crafty.math
* @sign public Boolean Crafty.math.withinRange(Number value, Number min, Number max)
* @param value - The specific value.
* @param min - Minimum value.
* @param max - Maximum value.
* @return Returns true if value is within a specific range.
*
* Check if a value is within a specific range.
*/
withinRange: function (value, min, max) {
return (value >= min && value <= max);
}
};
Crafty.math.Vector2D = (function () {
/**@
* #Crafty.math.Vector2D
*
* @class This is a general purpose 2D vector class
*
* Vector2D uses the following form:
* <x, y>
*
* @public
* @sign public {Vector2D} Vector2D();
* @sign public {Vector2D} Vector2D(Vector2D);
* @sign public {Vector2D} Vector2D(Number, Number);
* @param {Vector2D|Number=0} x
* @param {Number=0} y
*/
function Vector2D(x, y) {
if (x instanceof Vector2D) {
this.x = x.x;
this.y = x.y;
} else if (arguments.length === 2) {
this.x = x;
this.y = y;
} else if (arguments.length > 0)
throw "Unexpected number of arguments for Vector2D()";
} // class Vector2D
Vector2D.prototype.x = 0;
Vector2D.prototype.y = 0;
/**@
* #.add( )
*
* Adds the passed vector to this vector
*
* @public
* @sign public {Vector2D} add(Vector2D);
* @param {vector2D} vecRH
* @returns {Vector2D} this after adding
*/
Vector2D.prototype.add = function (vecRH) {
this.x += vecRH.x;
this.y += vecRH.y;
return this;
} // add( )
/**@
* #.angleBetween( )
*
* Calculates the angle between the passed vector and this vector, using <0,0> as the point of reference.
* Angles returned have the range (??, ?].
*
* @public
* @sign public {Number} angleBetween(Vector2D);
* @param {Vector2D} vecRH
* @returns {Number} the angle between the two vectors in radians
*/
Vector2D.prototype.angleBetween = function (vecRH) {
return Math.atan2(this.x * vecRH.y - this.y * vecRH.x, this.x * vecRH.x + this.y * vecRH.y);
} // angleBetween( )
/**@
* #.angleTo( )
*
* Calculates the angle to the passed vector from this vector, using this vector as the point of reference.
*
* @public
* @sign public {Number} angleTo(Vector2D);
* @param {Vector2D} vecRH
* @returns {Number} the angle to the passed vector in radians
*/
Vector2D.prototype.angleTo = function (vecRH) {
return Math.atan2(vecRH.y - this.y, vecRH.x - this.x);
};
/**@
* #.clone( )
*
* Creates and exact, numeric copy of this vector
*
* @public
* @sign public {Vector2D} clone();
* @returns {Vector2D} the new vector
*/
Vector2D.prototype.clone = function () {
return new Vector2D(this);
} // clone( )
/**@
* #.distance( )
*
* Calculates the distance from this vector to the passed vector.
*
* @public
* @sign public {Number} distance(Vector2D);
* @param {Vector2D} vecRH
* @returns {Number} the distance between the two vectors
*/
Vector2D.prototype.distance = function (vecRH) {
return Math.sqrt((vecRH.x - this.x) * (vecRH.x - this.x) + (vecRH.y - this.y) * (vecRH.y - this.y));
} // distance( )
/**@
* #.distanceSq( )
*
* Calculates the squared distance from this vector to the passed vector.
* This function avoids calculating the square root, thus being slightly faster than .distance( ).
*
* @public
* @sign public {Number} distanceSq(Vector2D);
* @param {Vector2D} vecRH
* @returns {Number} the squared distance between the two vectors
* @see Vector2D.distance( )
*/
Vector2D.prototype.distanceSq = function (vecRH) {
return (vecRH.x - this.x) * (vecRH.x - this.x) + (vecRH.y - this.y) * (vecRH.y - this.y);
} // distanceSq( )
/**@
* #.divide( )
*
* Divides this vector by the passed vector.
*
* @public
* @sign public {Vector2D} divide(Vector2D);
* @param {Vector2D} vecRH
* @returns {Vector2D} this vector after dividing
*/
Vector2D.prototype.divide = function (vecRH) {
this.x /= vecRH.x;
this.y /= vecRH.y;
return this;
} // divide( )
/**@
* #.dotProduct( )
*
* Calculates the dot product of this and the passed vectors
*
* @public
* @sign public {Number} dotProduct(Vector2D);
* @param {Vector2D} vecRH
* @returns {Number} the resultant dot product
*/
Vector2D.prototype.dotProduct = function (vecRH) {
return this.x * vecRH.x + this.y * vecRH.y;
} // dotProduct( )
/**@
* #.equals( )
*
* Determines if this vector is numerically equivalent to the passed vector.
*
* @public
* @sign public {Boolean} equals(Vector2D);
* @param {Vector2D} vecRH
* @returns {Boolean} true if the vectors are equivalent
*/
Vector2D.prototype.equals = function (vecRH) {
return vecRH instanceof Vector2D &&
this.x == vecRH.x && this.y == vecRH.y;
} // equals( )
/**@
* #.getNormal( )
*
* Calculates a new right-handed normal vector for the line created by this and the passed vectors.
*
* @public
* @sign public {Vector2D} getNormal([Vector2D]);
* @param {Vector2D=<0,0>} [vecRH]
* @returns {Vector2D} the new normal vector
*/
Vector2D.prototype.getNormal = function (vecRH) {
if (vecRH === undefined)
return new Vector2D(-this.y, this.x); // assume vecRH is <0, 0>
return new Vector2D(vecRH.y - this.y, this.x - vecRH.x).normalize();
} // getNormal( )
/**@
* #.isZero( )
*
* Determines if this vector is equal to <0,0>
*
* @public
* @sign public {Boolean} isZero();
* @returns {Boolean} true if this vector is equal to <0,0>
*/
Vector2D.prototype.isZero = function () {
return this.x === 0 && this.y === 0;
} // isZero( )
/**@
* #.magnitude( )
*
* Calculates the magnitude of this vector.
* Note: Function objects in JavaScript already have a 'length' member, hence the use of magnitude instead.
*
* @public
* @sign public {Number} magnitude();
* @returns {Number} the magnitude of this vector
*/
Vector2D.prototype.magnitude = function () {
return Math.sqrt(this.x * this.x + this.y * this.y);
} // magnitude( )
/**@
* #.magnitudeSq( )
*
* Calculates the square of the magnitude of this vector.
* This function avoids calculating the square root, thus being slightly faster than .magnitude( ).
*
* @public
* @sign public {Number} magnitudeSq();
* @returns {Number} the square of the magnitude of this vector
* @see Vector2D.magnitude( )
*/
Vector2D.prototype.magnitudeSq = function () {
return this.x * this.x + this.y * this.y;
} // magnitudeSq( )
/**@
* #.multiply( )
*
* Multiplies this vector by the passed vector
*
* @public
* @sign public {Vector2D} multiply(Vector2D);
* @param {Vector2D} vecRH
* @returns {Vector2D} this vector after multiplying
*/
Vector2D.prototype.multiply = function (vecRH) {
this.x *= vecRH.x;
this.y *= vecRH.y;
return this;
} // multiply( )
/**@
* #.negate( )
*
* Negates this vector (ie. <-x,-y>)
*
* @public
* @sign public {Vector2D} negate();
* @returns {Vector2D} this vector after negation
*/
Vector2D.prototype.negate = function () {
this.x = -this.x;
this.y = -this.y;
return this;
} // negate( )
/**@
* #.normalize( )
*
* Normalizes this vector (scales the vector so that its new magnitude is 1)
* For vectors where magnitude is 0, <1,0> is returned.
*
* @public
* @sign public {Vector2D} normalize();
* @returns {Vector2D} this vector after normalization
*/
Vector2D.prototype.normalize = function () {
var lng = Math.sqrt(this.x * this.x + this.y * this.y);
if (lng === 0) {
// default due East
this.x = 1;
this.y = 0;
} else {
this.x /= lng;
this.y /= lng;
} // else
return this;
} // normalize( )
/**@
* #.scale( )
*
* Scales this vector by the passed amount(s)
* If scalarY is omitted, scalarX is used for both axes
*
* @public
* @sign public {Vector2D} scale(Number[, Number]);
* @param {Number} scalarX
* @param {Number} [scalarY]
* @returns {Vector2D} this after scaling
*/
Vector2D.prototype.scale = function (scalarX, scalarY) {
if (scalarY === undefined)
scalarY = scalarX;
this.x *= scalarX;
this.y *= scalarY;
return this;
} // scale( )
/**@
* #.scaleToMagnitude( )
*
* Scales this vector such that its new magnitude is equal to the passed value.
*
* @public
* @sign public {Vector2D} scaleToMagnitude(Number);
* @param {Number} mag
* @returns {Vector2D} this vector after scaling
*/
Vector2D.prototype.scaleToMagnitude = function (mag) {
var k = mag / this.magnitude();
this.x *= k;
this.y *= k;
return this;
} // scaleToMagnitude( )
/**@
* #.setValues( )
*
* Sets the values of this vector using a passed vector or pair of numbers.
*
* @public
* @sign public {Vector2D} setValues(Vector2D);
* @sign public {Vector2D} setValues(Number, Number);
* @param {Number|Vector2D} x
* @param {Number} y
* @returns {Vector2D} this vector after setting of values
*/
Vector2D.prototype.setValues = function (x, y) {
if (x instanceof Vector2D) {
this.x = x.x;
this.y = x.y;
} else {
this.x = x;
this.y = y;
} // else
return this;
} // setValues( )
/**@
* #.subtract( )
*
* Subtracts the passed vector from this vector.
*
* @public
* @sign public {Vector2D} subtract(Vector2D);
* @param {Vector2D} vecRH
* @returns {vector2D} this vector after subtracting
*/
Vector2D.prototype.subtract = function (vecRH) {
this.x -= vecRH.x;
this.y -= vecRH.y;
return this;
} // subtract( )
/**@
* #.toString( )
*
* Returns a string representation of this vector.
*
* @public
* @sign public {String} toString();
* @returns {String}
*/
Vector2D.prototype.toString = function () {
return "Vector2D(" + this.x + ", " + this.y + ")";
} // toString( )
/**@
* #.translate( )
*
* Translates (moves) this vector by the passed amounts.
* If dy is omitted, dx is used for both axes.
*
* @public
* @sign public {Vector2D} translate(Number[, Number]);
* @param {Number} dx
* @param {Number} [dy]
* @returns {Vector2D} this vector after translating
*/
Vector2D.prototype.translate = function (dx, dy) {
if (dy === undefined)
dy = dx;
this.x += dx;
this.y += dy;
return this;
} // translate( )
/**@
* #.tripleProduct( )
*
* Calculates the triple product of three vectors.
* triple vector product = b(a?c) - a(b?c)
*
* @public
* @static
* @sign public {Vector2D} tripleProduct(Vector2D, Vector2D, Vector2D);
* @param {Vector2D} a
* @param {Vector2D} b
* @param {Vector2D} c
* @return {Vector2D} the triple product as a new vector
*/
Vector2D.tripleProduct = function (a, b, c) {
var ac = a.dotProduct(c);
var bc = b.dotProduct(c);
return new Crafty.math.Vector2D(b.x * ac - a.x * bc, b.y * ac - a.y * bc);
};
return Vector2D;
})();
Crafty.math.Matrix2D = (function () {
/**@
* #Crafty.math.Matrix2D
*
* @class This is a 2D Matrix2D class. It is 3x3 to allow for affine transformations in 2D space.
* The third row is always assumed to be [0, 0, 1].
*
* Matrix2D uses the following form, as per the whatwg.org specifications for canvas.transform():
* [a, c, e]
* [b, d, f]
* [0, 0, 1]
*
* @public
* @sign public {Matrix2D} new Matrix2D();
* @sign public {Matrix2D} new Matrix2D(Matrix2D);
* @sign public {Matrix2D} new Matrix2D(Number, Number, Number, Number, Number, Number);
* @param {Matrix2D|Number=1} a
* @param {Number=0} b
* @param {Number=0} c
* @param {Number=1} d
* @param {Number=0} e
* @param {Number=0} f
*/
Matrix2D = function (a, b, c, d, e, f) {
if (a instanceof Matrix2D) {
this.a = a.a;
this.b = a.b;
this.c = a.c;
this.d = a.d;
this.e = a.e;
this.f = a.f;
} else if (arguments.length === 6) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
} else if (arguments.length > 0)
throw "Unexpected number of arguments for Matrix2D()";
} // class Matrix2D
Matrix2D.prototype.a = 1;
Matrix2D.prototype.b = 0;
Matrix2D.prototype.c = 0;
Matrix2D.prototype.d = 1;
Matrix2D.prototype.e = 0;
Matrix2D.prototype.f = 0;
/**@
* #.apply( )
*
* Applies the matrix transformations to the passed object
*
* @public
* @sign public {Vector2D} apply(Vector2D);
* @param {Vector2D} vecRH - vector to be transformed
* @returns {Vector2D} the passed vector object after transforming
*/
Matrix2D.prototype.apply = function (vecRH) {
// I'm not sure of the best way for this function to be implemented. Ideally
// support for other objects (rectangles, polygons, etc) should be easily
// addable in the future. Maybe a function (apply) is not the best way to do
// this...?
var tmpX = vecRH.x;
vecRH.x = tmpX * this.a + vecRH.y * this.c + this.e;
vecRH.y = tmpX * this.b + vecRH.y * this.d + this.f;
// no need to homogenize since the third row is always [0, 0, 1]
return vecRH;
} // apply( )
/**@
* #.clone( )
*
* Creates an exact, numeric copy of the current matrix
*
* @public
* @sign public {Matrix2D} clone();
* @returns {Matrix2D}
*/
Matrix2D.prototype.clone = function () {
return new Matrix2D(this);
} // clone( )
/**@
* #.combine( )
*
* Multiplies this matrix with another, overriding the values of this matrix.
* The passed matrix is assumed to be on the right-hand side.
*
* @public
* @sign public {Matrix2D} combine(Matrix2D);
* @param {Matrix2D} mtrxRH
* @returns {Matrix2D} this matrix after combination
*/
Matrix2D.prototype.combine = function (mtrxRH) {
var tmp = this.a;
this.a = tmp * mtrxRH.a + this.b * mtrxRH.c;
this.b = tmp * mtrxRH.b + this.b * mtrxRH.d;
tmp = this.c;
this.c = tmp * mtrxRH.a + this.d * mtrxRH.c;
this.d = tmp * mtrxRH.b + this.d * mtrxRH.d;
tmp = this.e;
this.e = tmp * mtrxRH.a + this.f * mtrxRH.c + mtrxRH.e;
this.f = tmp * mtrxRH.b + this.f * mtrxRH.d + mtrxRH.f;
return this;
} // combine( )
/**@
* #.equals( )
*
* Checks for the numeric equality of this matrix versus another.
*
* @public
* @sign public {Boolean} equals(Matrix2D);
* @param {Matrix2D} mtrxRH
* @returns {Boolean} true if the two matrices are numerically equal
*/
Matrix2D.prototype.equals = function (mtrxRH) {
return mtrxRH instanceof Matrix2D &&
this.a == mtrxRH.a && this.b == mtrxRH.b && this.c == mtrxRH.c &&
this.d == mtrxRH.d && this.e == mtrxRH.e && this.f == mtrxRH.f;
} // equals( )
/**@
* #.determinant( )
*
* Calculates the determinant of this matrix
*
* @public
* @sign public {Number} determinant();
* @returns {Number} det(this matrix)
*/
Matrix2D.prototype.determinant = function () {
return this.a * this.d - this.b * this.c;
} // determinant( )
/**@
* #.invert( )
*
* Inverts this matrix if possible
*
* @public
* @sign public {Matrix2D} invert();
* @returns {Matrix2D} this inverted matrix or the original matrix on failure
* @see Matrix2D.isInvertible( )
*/
Matrix2D.prototype.invert = function () {
var det = this.determinant();
// matrix is invertible if its determinant is non-zero
if (det !== 0) {
var old = {
a: this.a,
b: this.b,
c: this.c,
d: this.d,
e: this.e,
f: this.f
};
this.a = old.d / det;
this.b = -old.b / det;
this.c = -old.c / det;
this.d = old.a / det;
this.e = (old.c * old.f - old.e * old.d) / det;
this.f = (old.e * old.b - old.a * old.f) / det;
} // if
return this;
} // invert( )
/**@
* #.isIdentity( )
*
* Returns true if this matrix is the identity matrix
*
* @public
* @sign public {Boolean} isIdentity();
* @returns {Boolean}
*/
Matrix2D.prototype.isIdentity = function () {
return this.a === 1 && this.b === 0 && this.c === 0 && this.d === 1 && this.e === 0 && this.f === 0;
} // isIdentity( )
/**@
* #.isInvertible( )
*
* Determines is this matrix is invertible.
*
* @public
* @sign public {Boolean} isInvertible();
* @returns {Boolean} true if this matrix is invertible
* @see Matrix2D.invert( )
*/
Matrix2D.prototype.isInvertible = function () {
return this.determinant() !== 0;
} // isInvertible( )
/**@
* #.preRotate( )
*
* Applies a counter-clockwise pre-rotation to this matrix
*
* @public
* @sign public {Matrix2D} preRotate(Number);
* @param {number} rads - angle to rotate in radians
* @returns {Matrix2D} this matrix after pre-rotation
*/
Matrix2D.prototype.preRotate = function (rads) {
var nCos = Math.cos(rads);
var nSin = Math.sin(rads);
var tmp = this.a;
this.a = nCos * tmp - nSin * this.b;
this.b = nSin * tmp + nCos * this.b;
tmp = this.c;
this.c = nCos * tmp - nSin * this.d;
this.d = nSin * tmp + nCos * this.d;
return this;
} // preRotate( )
/**@
* #.preScale( )
*
* Applies a pre-scaling to this matrix
*
* @public
* @sign public {Matrix2D} preScale(Number[, Number]);
* @param {Number} scalarX
* @param {Number} [scalarY] scalarX is used if scalarY is undefined
* @returns {Matrix2D} this after pre-scaling
*/
Matrix2D.prototype.preScale = function (scalarX, scalarY) {
if (scalarY === undefined)
scalarY = scalarX;
this.a *= scalarX;
this.b *= scalarY;
this.c *= scalarX;
this.d *= scalarY;
return this;
} // preScale( )
/**@
* #.preTranslate( )
*
* Applies a pre-translation to this matrix
*
* @public
* @sign public {Matrix2D} preTranslate(Vector2D);
* @sign public {Matrix2D} preTranslate(Number, Number);
* @param {Number|Vector2D} dx
* @param {Number} dy
* @returns {Matrix2D} this matrix after pre-translation
*/
Matrix2D.prototype.preTranslate = function (dx, dy) {
if (typeof dx === "number") {
this.e += dx;
this.f += dy;
} else {
this.e += dx.x;
this.f += dx.y;
} // else
return this;
} // preTranslate( )
/**@
* #.rotate( )
*
* Applies a counter-clockwise post-rotation to this matrix
*
* @public
* @sign public {Matrix2D} rotate(Number);
* @param {Number} rads - angle to rotate in radians
* @returns {Matrix2D} this matrix after rotation
*/
Matrix2D.prototype.rotate = function (rads) {
var nCos = Math.cos(rads);
var nSin = Math.sin(rads);
var tmp = this.a;
this.a = nCos * tmp - nSin * this.b;
this.b = nSin * tmp + nCos * this.b;
tmp = this.c;
this.c = nCos * tmp - nSin * this.d;
this.d = nSin * tmp + nCos * this.d;
tmp = this.e;
this.e = nCos * tmp - nSin * this.f;
this.f = nSin * tmp + nCos * this.f;
return this;
} // rotate( )
/**@
* #.scale( )
*
* Applies a post-scaling to this matrix
*
* @public
* @sign public {Matrix2D} scale(Number[, Number]);
* @param {Number} scalarX
* @param {Number} [scalarY] scalarX is used if scalarY is undefined
* @returns {Matrix2D} this after post-scaling
*/
Matrix2D.prototype.scale = function (scalarX, scalarY) {
if (scalarY === undefined)
scalarY = scalarX;
this.a *= scalarX;
this.b *= scalarY;
this.c *= scalarX;
this.d *= scalarY;
this.e *= scalarX;
this.f *= scalarY;
return this;
} // scale( )
/**@
* #.setValues( )
*
* Sets the values of this matrix
*
* @public
* @sign public {Matrix2D} setValues(Matrix2D);
* @sign public {Matrix2D} setValues(Number, Number, Number, Number, Number, Number);
* @param {Matrix2D|Number} a
* @param {Number} b
* @param {Number} c
* @param {Number} d
* @param {Number} e
* @param {Number} f
* @returns {Matrix2D} this matrix containing the new values
*/
Matrix2D.prototype.setValues = function (a, b, c, d, e, f) {
if (a instanceof Matrix2D) {
this.a = a.a;
this.b = a.b;
this.c = a.c;
this.d = a.d;
this.e = a.e;
this.f = a.f;
} else {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
} // else
return this;
} // setValues( )
/**@
* #.toString( )
*
* Returns the string representation of this matrix.
*
* @public
* @sign public {String} toString();
* @returns {String}
*/
Matrix2D.prototype.toString = function () {
return "Matrix2D([" + this.a + ", " + this.c + ", " + this.e +
"] [" + this.b + ", " + this.d + ", " + this.f + "] [0, 0, 1])";
} // toString( )
/**@
* #.translate( )
*
* Applies a post-translation to this matrix
*
* @public
* @sign public {Matrix2D} translate(Vector2D);
* @sign public {Matrix2D} translate(Number, Number);
* @param {Number|Vector2D} dx
* @param {Number} dy
* @returns {Matrix2D} this matrix after post-translation
*/
Matrix2D.prototype.translate = function (dx, dy) {
if (typeof dx === "number") {
this.e += this.a * dx + this.c * dy;
this.f += this.b * dx + this.d * dy;
} else {
this.e += this.a * dx.x + this.c * dx.y;
this.f += this.b * dx.x + this.d * dx.y;
} // else
return this;
} // translate( )
return Matrix2D;
})();
/**@
* #Crafty Time
* @category Utilities
*/
Crafty.c("Delay", {
init : function() {
this._delays = [];
this.bind("EnterFrame", function() {
var now = new Date().getTime();
for(var index in this._delays) {
var item = this._delays[index];
if(!item.triggered && item.start + item.delay + item.pause < now) {
item.triggered=true;
item.func.call(this);
}
}
});
this.bind("Pause", function() {
var now = new Date().getTime();
for(var index in this._delays) {
this._delays[index].pauseBuffer = now;
}
});
this.bind("Unpause", function() {
var now = new Date().getTime();
for(var index in this._delays) {
var item = this._delays[index];
item.pause += now-item.pauseBuffer;
}
});
},
/**@
* #.delay
* @comp Crafty Time
* @sign public this.delay(Function callback, Number delay)
* @param callback - Method to execute after given amount of milliseconds
* @param delay - Amount of milliseconds to execute the method
*
* The delay method will execute a function after a given amount of time in milliseconds.
*
* It is not a wrapper for `setTimeout`.
*
* If Crafty is paused, the delay is interrupted with the pause and then resume when unpaused
*
* If the entity is destroyed, the delay is also destroyed and will not have effect.
*
* @example
* ~~~
* console.log("start");
* this.delay(function() {
console.log("100ms later");
* }, 100);
* ~~~
*/
delay : function(func, delay) {
return this._delays.push({
start : new Date().getTime(),
func : func,
delay : delay,
triggered : false,
pauseBuffer: 0,
pause: 0
});
}
});
})(Crafty,window,window.document);
|