Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/*
Copyright (c) 2011 Batiste Bieler and contributors,
https://github.com/batiste/sprite.js
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*jslint bitwise: true, undef: true, white: true, maxerr: 50, indent: 4 */
/* Sprite.js v1.2.1
*
* coding guideline
*
* CamelCase everywhere (I don't like it but it seems to the standard these days).
* Tabs have to be 4 spaces (python style).
* If you contribute don't forget to add your name in the AUTHORS file.
*/
(function (global) {
"use strict";
var sjs, Sprite, Scene, Layer, Ticker, Ticker_, Cycle, Input, _Input, List,
doc = global.document,
// number of sprites
nb_sprite = 0,
// number of scenes
nb_scene = 0,
// number of cycle
nb_cycle = 0,
browser_specific_runned = false,
// global z-index
zindex = 1;
//IE 8 fix help functions
function _addEventListener(element, type,listener,useCapture){
if(element.addEventListener){
element.addEventListener(type, listener, useCapture);
}else if(element.attachEvent){
element.attachEvent("on" + type, listener);
}
}
function _removeEventListener(element, type,listener,useCapture){
if(element.removeEventListener){
element.removeEventListener(type, listener, useCapture);
}else if (element.detachEvent){
element.detachEvent(type, listener);
}
}
function _preventEvent(e){
if (e.preventDefault) {
e.preventDefault();
e.stopPropagation();
}else{
e.returnValue = false;
}
}
// math functions
function mod(n, base) {
// strictly positive modulo
return ((n % base) + base) % base;
}
function hypo(x, y) {
return Math.sqrt(x * x + y * y);
}
function normalVector(vx, vy, intensity) {
var n = hypo(vx, vy);
if (n === 0) {
return {x: vx, y: vy};
}
if (intensity) {
return {x: ((vx / n) * intensity), y: ((vy / n) * intensity)};
}
return {x: vx / n, y: vy / n};
}
function lineSide(ax, ay, bx, by, cx, cy) {
// return true if the point C is on the right of the line (A, B)
var v = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
if (v === 0) {
return null;
}
return v > 0;
}
// browser specific feature detection
function has(el, propList) {
var prop = propList.shift();
while (prop) {
if (typeof el[prop] !== 'undefined') {
return prop;
}
prop = propList.shift();
}
}
function initBrowserSpecific() {
sjs.tproperty = has(doc.body.style, [
'transform',
'webkitTransform',
'MozTransform',
'OTransform',
'msTransform']);
sjs.requestAnimationFrame = has(global, [
'requestAnimationFrame',
'mozRequestAnimationFrame',
'webkitRequestAnimationFrame',
'oRequestAnimationFrame',
'msRequestAnimationFrame']);
sjs.cancelAnimationFrame = has(global, [
'cancelAnimationFrame',
'cancelRequestAnimationFrame',
'mozCancelAnimationFrame',
'mozCancelRequestAnimationFrame',
'webkitCancelAnimationFrame',
'webkitCancelRequestAnimationFrame',
'oCancelAnimationFrame',
'oCancelRequestAnimationFrame',
'msCancelAnimationFrame',
'msCancelRequestAnimationFrame']);
sjs.createEventProperty = has(doc, ['createEvent', 'createEventObject']);
browser_specific_runned = true;
}
function optionValue(options, name, default_value, type) {
if (options && options[name] !== undefined) {
if (type === 'int') {
return options[name] | 0;
}
return options[name];
}
return default_value;
}
function overlay(x, y, w, h) {
var div = doc.createElement('div'),
s = div.style;
s.top = y + 'px';
s.left = x + 'px';
s.width = w + 'px';
s.height = h + 'px';
s.color = '#fff';
s.zIndex = 100;
s.position = 'absolute';
s.backgroundColor = '#000';
s.opacity = 0.7;
return div;
}
Scene = function Scene(options) {
if (this.constructor !== Scene) {
return new Scene(options);
}
if (!browser_specific_runned) {
initBrowserSpecific();
}
this.autoPause = optionValue(options, 'autoPause', true);
// main function
this.main = optionValue(options, 'main', function () {});
var div = doc.createElement('div'), parent;
div.style.overflow = 'hidden';
// TODO: detect those features
// image-rendering: -moz-crisp-edges;
// ms-interpolation-mode: nearest-neighbor;
div.style.imageRendering = '-webkit-optimize-contrast';
div.style.position = 'relative';
div.className = 'sjs';
div.id = 'sjs' + nb_scene;
this.id = nb_scene;
nb_scene = nb_scene + 1;
parent = optionValue(options, 'parent', doc.body);
parent.appendChild(div);
this.w = optionValue(options, 'w', 480, 'int');
this.h = optionValue(options, 'h', 320, 'int');
this.dom = div;
this.dom.style.width = this.w + 'px';
this.dom.style.height = this.h + 'px';
this.layers = {};
this.ticker = null;
this.useCanvas = optionValue(options, "useCanvas",
global.location.href.indexOf('canvas') !== -1);
this.xscale = 1;
this.yscale = 1;
// needs to be done after this.useCanvas
this.Layer("default");
sjs.scenes.push(this);
return this;
};
Scene.prototype.constructor = Scene;
Scene.prototype.Sprite = function SceneSprite(src, layer) {
// A shortcut for sjs.Sprite
if(layer===undefined)
sjs.error("When you create Sprite from the scene the layer should be specified or false.");
return new Sprite(this, src, layer);
};
Scene.prototype.Layer = function SceneLayer(name, options) {
return new Layer(this, name, options);
};
// just for convenience
Scene.prototype.Cycle = function SceneCycle(triplets) {
return new Cycle(triplets);
};
Scene.prototype.Input = function SceneInput() {
this.input = new Input(this);
return this.input;
};
Scene.prototype.scale = function SceneScale(x, y) {
this.xscale = x;
this.yscale = y;
this.dom.style[sjs.tproperty+"Origin"] = "0 0";
this.dom.style[sjs.tproperty] = "scale(" + x + "," + y + ")";
};
Scene.prototype.toString = function () {
return "Scene(" + String(this.id) + ")";
};
Scene.prototype.reset = function reset() {
var l;
if (this.ticker) {
this.ticker.pause();
}
for (l in this.layers) {
if (this.layers.hasOwnProperty(l)) {
this.layers[l].dom.parentNode.removeChild(this.layers[l].dom);
delete this.layers[l];
}
}
// remove remaining children
while (this.dom.childNodes.length >= 1) {
this.dom.removeChild(this.dom.firstChild);
}
this.layers = {};
this.Layer("default");
};
Scene.prototype.Ticker = function Ticker(paint, options) {
if (this.ticker) {
this.ticker.pause();
this.ticker.paint = function () {};
}
this.ticker = new Ticker_(this, paint, options);
return this.ticker;
};
Scene.prototype.loadImages = function loadImages(images, callback) {
// function used to preload the sprite images
if (!callback) {
callback = this.main;
}
var toLoad = 0, total, div, img, src, error, scene, i;
for (i = 0; i < images.length; i++) {
if (!sjs.spriteCache[images[i]]) {
toLoad += 1;
sjs.spriteCache[images[i]] = {src: images[i], loaded: false, loading: false};
}
}
if (toLoad === 0) {
return callback();
}
total = toLoad;
div = overlay(0, 0, this.w, this.h);
div.style.textAlign = 'center';
div.style.paddingTop = (this.h / 2 - 16) + 'px';
div.innerHTML = 'Loading';
this.dom.appendChild(div);
scene = this;
error = false;
var _loadImg = function(src) {
sjs.spriteCache[src].loading = true;
img = doc.createElement('img');
sjs.spriteCache[src].img = img;
_addEventListener(img, 'load', function () {
sjs.spriteCache[src].loaded = true;
toLoad -= 1;
if (error === false) {
if (toLoad === 0) {
scene.dom.removeChild(div);
callback();
} else {
div.innerHTML = 'Loading ' + ((total - toLoad) / total * 100 | 0) + '%';
}
}
}, false);
_addEventListener(img, 'error', function () {
error = true;
div.innerHTML = 'Error loading image ' + src;
}, false);
img.src = src;
}
for (src in sjs.spriteCache) {
if (sjs.spriteCache.hasOwnProperty(src)) {
if (!sjs.spriteCache[src].loading) {
_loadImg(src);
}
}
}
};
Sprite = function Sprite(scene, src, layer) {
this.scene = scene;
this._dirty = {};
this.changed = false;
// positions
this.y = 0;
this.x = 0;
this._x_before = 0;
this._x_rounded = 0;
this._y_before = 0;
this._y_rounded = 0;
//velocity
this.xv = 0;
this.yv = 0;
this.rv = 0;
// shape: rectangle, circle
this.type = "rectangle";
// newton
this.mass = 1;
this.friction = 0.05;
// forces
this.xf = 0;
this.yf = 0;
// image
this.src = null;
this.img = null;
this.imgNaturalWidth = null;
this.imgNaturalHeight = null;
// width and height of the sprite view port
this.w = null;
this.h = null;
// offsets of the image within the viewport
this.xoffset = 0;
this.yoffset = 0;
this.dom = null;
this.cycle = null;
this.xscale = 1;
this.yscale = 1;
this.angle = 0;
this.xTransformOrigin = null;
this.yTransformOrigin = null;
this.backgroundRepeat = null;
this.opacity = 1;
this.color = false;
this.id = ++nb_sprite;
// necessary to get set
this.layer = null;
var value, target, setF, first_char, d, p, properties;
// if it doesn't seems to kouak like a Layer object
if (layer) {
// this is a layer object
if (layer.sprites) {
this.layer = layer;
} else {
// we can receive things like this
// {x: 10, y: 10, w: 10, h: 50, size: [20, 30], layer: var}
properties = layer;
// this is the messy magic options initializer code
for (p in properties) {
if (properties.hasOwnProperty(p)) {
value = properties[p];
target = this[p];
if (typeof target === "function") {
this[p].apply(this, value);
}
else if (target !== undefined) {
// this is necessary to set cache value properly
first_char = p.charAt(0);
if ((first_char === 'x' || first_char === 'y') && p.length > 1) {
setF = 'set' + first_char.toUpperCase() + p.charAt(1).toUpperCase() + p.slice(2);
} else {
setF = 'set' + first_char.toUpperCase() + p.slice(1);
}
if (this[setF]) {
this[setF].apply(this, [value]);
} else {
// necessary for layer option
this[p] = value;
}
}
}
}
}
}
// can be set by the properties
if (this.layer === undefined || layer === undefined) {
this.layer = scene.layers['default'];
}
if (this.layer && !this.layer.useCanvas) {
d = doc.createElement('div');
d.style.position = 'absolute';
this.dom = d;
this.layer.dom.appendChild(d);
}
if (src) {
this.loadImg(src);
}
return this;
};
Sprite.prototype.constructor = Sprite;
/* boilerplate setter functions */
Sprite.prototype.setX = function setX(value) {
this.x = value;
// this secessary for the physic
this._x_rounded = value | 0;
this.changed = true;
return this;
};
Sprite.prototype.setY = function setY(value) {
this.y = value;
this._y_rounded = value | 0;
this.changed = true;
return this;
};
Sprite.prototype.setW = function setW(value) {
this.w = value;
this._dirty.w = true;
this.changed = true;
return this;
};
Sprite.prototype.setH = function setH(value) {
this.h = value;
this._dirty.h = true;
this.changed = true;
return this;
};
Sprite.prototype.setXOffset = function setXoffset(value) {
this.xoffset = value;
this._dirty.xoffset = true;
this.changed = true;
return this;
};
Sprite.prototype.setYOffset = function setYoffset(value) {
this.yoffset = value;
this._dirty.yoffset = true;
this.changed = true;
return this;
};
Sprite.prototype.setAngle = function setAngle(value) {
this.angle = value;
this._dirty.angle = true;
this.changed = true;
return this;
};
Sprite.prototype.setColor = function setColor(value) {
this.color = value;
this._dirty.color = true;
this.changed = true;
return this;
};
Sprite.prototype.setOpacity = function setOpacity(value) {
this.opacity = value;
this._dirty.opacity = true;
this.changed = true;
return this;
};
Sprite.prototype.setXScale = function setXscale(value) {
this.xscale = value;
this._dirty.xscale = true;
this.changed = true;
return this;
};
Sprite.prototype.setYScale = function setYscale(value) {
this.yscale = value;
this._dirty.yscale = true;
this.changed = true;
return this;
};
Sprite.prototype.transformOrigin = function transformOrigin(x, y) {
this.xTransformOrigin = x;
this.yTransformOrigin = y;
this._dirty.transform = true;
this.changed = true;
return this;
};
Sprite.prototype.setBackgroundRepeat = function setBackgroundRepeat(value) {
this._dirty.backgroundRepeat = true;
this.backgroundRepeat = value;
return this;
};
// End of boilerplate setters, start of helpers
Sprite.prototype.rotate = function (v) {
this.setAngle(this.angle + v);
return this;
};
Sprite.prototype.orient = function orient(x, y) {
var a = Math.atan2(y, x);
this.setAngle(a);
};
Sprite.prototype.scale = function (x, y) {
if (this.xscale !== x) {
this.setXScale(x);
}
if (y === undefined) {
y = x;
}
if (this.yscale !== y) {
this.setYScale(y);
}
return this;
};
Sprite.prototype.move = function (x, y) {
this.setX(this.x + x);
this.setY(this.y + y);
return this;
};
Sprite.prototype.position = function (x, y) {
this.setX(x);
this.setY(y);
return this;
};
Sprite.prototype.offset = function (x, y) {
this.setXOffset(x);
this.setYOffset(y);
return this;
};
Sprite.prototype.size = function (w, h) {
this.setW(w);
this.setH(h);
return this;
};
Sprite.prototype.toFront = function(){
this.layer.lastZIndex++;
return this.setZIndex(this.layer.lastZIndex);
};
Sprite.prototype.toBack = function(){
this.layer.lastZIndex++;
return this.setZIndex(-this.layer.lastZIndex);
};
Sprite.prototype.setZIndex = function(z){
if(this.dom && this.layer) {
this._dirty.zindex = true;
this.changed = true;
this.zindex = z;
}
return this;
};
// Physic
Sprite.prototype.setForce = function setForce(xf, yf) {
this.xf = xf;
this.yf = yf;
};
Sprite.prototype.addForce = function addForce(xf, yf) {
this.xf += xf;
this.yf += yf;
};
Sprite.prototype.applyForce = function applyForce(ticks) {
if (ticks === undefined) {
ticks = 1;
}
// Integrate newton's laws of motion F = ma => a = F / m
this.xv -= this.friction * this.xv * this.mass * ticks;
this.xv += (this.xf / this.mass) * ticks;
this.yv -= this.friction * this.yv * this.mass * ticks;
this.yv += (this.yf / this.mass) * ticks;
};
Sprite.prototype.velocity = function () {
return hypo(this.xv, this.yv);
};
Sprite.prototype.setVelocity = function (xv, yv) {
this.xv = xv;
this.yv = yv;
};
Sprite.prototype.addVelocity = function (xv, yv) {
this.xv += xv;
this.yv += yv;
};
Sprite.prototype.applyVelocity = function (ticks) {
if (ticks === undefined)
ticks = 1;
if (this.xv !== 0)
this.setX(this.x + this.xv * ticks);
if (this.yv !== 0)
this.setY(this.y + this.yv * ticks);
if (this.rv !== 0)
this.setAngle(this.angle + this.rv * ticks);
return this;
};
Sprite.prototype.reverseVelocity = function (ticks) {
if (ticks === undefined)
ticks = 1;
if (this.xv !== 0)
this.setX(this.x - this.xv * ticks);
if (this.yv !== 0)
this.setY(this.y - this.yv * ticks);
if (this.rv !== 0)
this.setAngle(this.angle - this.rv * ticks);
return this;
};
Sprite.prototype.applyXVelocity = function (ticks) {
if (ticks === undefined)
ticks = 1;
if (this.xv !== 0)
this.setX(this.x + this.xv * ticks);
};
Sprite.prototype.reverseXVelocity = function (ticks) {
if (ticks === undefined)
ticks = 1;
if (this.xv !== 0)
this.setX(this.x-this.xv * ticks);
};
Sprite.prototype.applyYVelocity = function (ticks) {
if (ticks === undefined)
ticks = 1;
if (this.yv !== 0)
this.setY(this.y+this.yv * ticks);
};
Sprite.prototype.reverseYVelocity = function (ticks) {
if (ticks === undefined)
ticks = 1;
if (this.yv !== 0)
this.setY(this.y-this.yv * ticks);
};
Sprite.prototype.rotateVelocity = function (a) {
var x = this.xv * Math.cos(a) - this.yv * Math.sin(a);
this.yv = this.xv * Math.sin(a) + this.yv * Math.cos(a);
this.xv = x;
};
Sprite.prototype.orientVelocity = function (x, y) {
var intensity = hypo(this.xv, this.yv), v;
v = normalVector(x, y, intensity);
this.xv = v.x;
this.yv = v.y;
};
Sprite.prototype.remove = function remove() {
if (this.cycle)
this.cycle.removeSprite(this);
if (this.layer && !this.layer.useCanvas) {
this.layer.dom.removeChild(this.dom);
this.dom = null;
}
if (this.texture)
this.texture.remove();
this.texture = null;
//delete this.layer.sprites[this.layerIndex];
this.layer = null;
this.img = null;
};
// Update methods
Sprite.prototype.webGLUpdate = function webGLUpdate () {
if (!this.texture) {
this.texture = new webgl.Texture(this);
}
this.texture.render(this.x, this.y);
return this;
};
Sprite.prototype.update = function updateDomProperties () {
if(this.layer.scene.disableUpdate)
return this;
// This is the CPU heavy function.
if (this.layer.useWebGL) {
return this.webGLUpdate();
}
if (this.layer.useCanvas) {
return this.canvasUpdate();
}
var style = this.dom.style, trans;
// using Math.round to round integers before changing seems to improve a bit performances
if (this._x_before !== this._x_rounded)
style.left=(this.x | 0) + 'px';
if (this._y_before !== this._y_rounded)
style.top=(this.y | 0) + 'px';
// cache rounded positions, it's used to avoid unecessary update
this._x_before = this._x_rounded;
this._y_before = this._y_rounded;
if (!this.changed)
return this;
if (this._dirty.w)
style.width=(this.w | 0) +'px';
if (this._dirty.h)
style.height=(this.h | 0) + 'px';
// translate and translate3d doesn't seems to offer any speedup
// in my tests.
if (this._dirty.xoffset || this._dirty.yoffset)
style.backgroundPosition=-(this.xoffset | 0) + 'px ' + -(this.yoffset | 0) + 'px';
if (this._dirty.opacity)
if ('opacity' in document.body.style) {
style.opacity = this.opacity;
} else {
style.filter = "alpha(opacity="+ this.opacity*100 + ")";
}
if (this._dirty.color)
style.backgroundColor = this.color;
if (this._dirty.zindex)
style.zIndex = this.zindex;
if(this._dirty.transform) {
style[sjs.tproperty + 'Origin'] = this.xTransformOrigin + " " + this.yTransformOrigin;
}
if(this._dirty.backgroundRepeat) {
style.backgroundRepeat = this.backgroundRepeat;
}
// those transformation have pretty bad perfs implication on Opera,
// don't update those values if nothing changed
if (this._dirty.xscale || this._dirty.yscale || this._dirty.angle) {
trans = "";
if (this.angle !== 0)
trans += 'rotate(' + this.angle + 'rad) ';
if (this.xscale !== 1 || this.yscale !== 1) {
trans += ' scale(' + this.xscale + ', ' + this.yscale + ')';
}
style[sjs.tproperty] = trans;
}
// reset
this.changed = false;
this._dirty = {};
return this;
};
Sprite.prototype.canvasUpdate = function canvasUpdate(layer) {
var ctx, transx, transy, repeat_w, repeat_y;
if (layer)
ctx = layer.ctx;
else
ctx = this.layer.ctx;
var fast_track = (
this.angle == 0
&& this.opacity == 1
&& this.imgNaturalWidth == this.w
&& this.imgNaturalHeight == this.h
&& this.xTransformOrigin === null
)
if(fast_track) {
ctx.drawImage(this.img, this.xoffset, this.yoffset, this.w, this.h,
this._x_rounded, this._y_rounded, this.w, this.h);
return this;
}
ctx.save();
if (this.xTransformOrigin === null) {
// 50% 505 in CSS
transx = this.w >> 1;
transy = this.h >> 1;
} else {
transx = this.xTransformOrigin;
transy = this.yTransformOrigin;
}
// rounding the coordinates yield a big performance improvement
ctx.translate(this._x_rounded + transx, this._y_rounded + transy);
ctx.rotate(this.angle);
if (this.xscale !== 1 || this.yscale !== 1)
ctx.scale(this.xscale, this.yscale);
ctx.globalAlpha = this.opacity;
ctx.translate(-transx, -transy);
// handle background colors.
if (this.color) {
ctx.fillStyle = this.color;
ctx.fillRect(0, 0, this.w, this.h);
}
// handle repeating images, a way to implement repeating background in canvas
if (this.imgLoaded && this.img) {
if (this.imgNaturalWidth < this.w || this.imgNaturalHeight < this.h) {
repeat_w = Math.floor(this.w / this.imgNaturalWidth);
while(repeat_w > 0) {
repeat_w = repeat_w-1;
repeat_y = Math.floor(this.h / this.imgNaturalHeight);
while(repeat_y > 0) {
repeat_y = repeat_y-1;
ctx.drawImage(this.img, this.xoffset, this.yoffset,
this.imgNaturalWidth,
this.imgNaturalHeight,
repeat_w * this.imgNaturalWidth,
repeat_y * this.imgNaturalHeight,
this.imgNaturalWidth,
this.imgNaturalHeight);
}
}
} else {
// image with normal size or with
ctx.drawImage(this.img, this.xoffset, this.yoffset, this.w, this.h, 0, 0, this.w, this.h);
}
}
ctx.restore();
return this;
};
// Other methods
Sprite.prototype.toString = function () {
return "Sprite(" + String(this.id) + ")";
};
Sprite.prototype.onload = function (callback) {
if (this.imgLoaded && this._callback) {
this._callback = callback;
}
};
Sprite.prototype.loadImg = function (src, resetSize) {
// the image exact source value will change according to the
// hostname, this is useful to retain the original source value here.
var _loaded, there = this, img;
this.src = src;
// check if the image is already in the cache
if (!sjs.spriteCache[src]) {
// if not we create the image in the cache
this.img = doc.createElement('img');
sjs.spriteCache[src] = {src: src, img: this.img, loaded: false, loading: true};
_loaded = false;
} else {
// if it's already there, we set img object and check if it's loaded
this.img = sjs.spriteCache[src].img;
_loaded = sjs.spriteCache[src].loaded;
}
// actions to perform when the image is loaded
function imageReady(e) {
img = there.img;
sjs.spriteCache[src].loaded = true;
there.imgLoaded = true;
if (there.layer && !there.layer.useCanvas)
there.dom.style.backgroundImage = 'url(' + src + ')';
there.imgNaturalWidth = img.width;
there.imgNaturalHeight = img.height;
if (there.w === null || resetSize)
there.setW(img.width);
if (there.h === null || resetSize)
there.setH(img.height);
there.onload();
}
if (_loaded)
imageReady();
else {
_addEventListener(this.img, 'load', imageReady, false);
this.img.src = src;
}
return this;
};
Sprite.prototype.distance = function distance(x, y) {
// Return the distance between this sprite and the point (x, y) or a Sprite
if (typeof x === "number") {
return Math.sqrt(Math.pow(this.x + this.w / 2 - x, 2) +
Math.pow(this.y + this.h / 2 - y, 2));
} else {
return Math.sqrt(Math.pow(this.x + (this.w / 2) - (x.x + (x.w / 2)), 2) +
Math.pow(this.y + (this.h / 2) - (x.y + (x.h / 2)), 2));
}
};
Sprite.prototype.center = function center() {
return {x: this.x + this.w / 2, y: this.y + this.h / 2};
};
// Fx
Sprite.prototype.explode2 = function explode(v, horizontal, layer) {
if (!layer)
layer = this.layer;
var props = {layer:layer, color:this.color};
if (v === undefined) {
if (horizontal)
v = this.h >> 1;
else
v = this.w >> 1;
}
var s1 = layer.scene.Sprite(this.src, props);
var s2 = layer.scene.Sprite(this.src, props);
if (horizontal) {
s1.size(this.w, v);
s1.position(this.x, this.y);
s2.size(this.w, this.h - v);
s2.position(this.x, this.y + v);
s2.setYOffset(v);
} else {
s1.size(v, this.h);
s1.position(this.x, this.y);
s2.size(this.w - v, this.h);
s2.position(this.x + v, this.y);
s2.setXOffset(v);
}
return [s1, s2];
};
Sprite.prototype.explode4 = function explode(x, y, layer) {
if (x === undefined)
x = this.w >> 1;
if (y === undefined)
y = this.h >> 1;
if (!layer)
layer = this.layer;
var props = {layer:layer, color:this.color};
// top left sprite, going counterclockwise
var s1 = layer.scene.Sprite(this.src, props),
s2 = layer.scene.Sprite(this.src, props),
s3 = layer.scene.Sprite(this.src, props),
s4 = layer.scene.Sprite(this.src, props);
s1.size(x, y);
s1.position(this.x, this.y);
s2.size(this.w - x, y);
s2.position(this.x + x, this.y);
s2.offset(x, 0);
s3.size(this.w - x, this.h - y);
s3.position(this.x + x, this.y + y);
s3.offset(x, y);
s4.size(x, this.h - y);
s4.position(this.x, this.y + y);
s4.offset(0, y);
return [s1, s2, s3, s4];
};
Cycle = function Cycle(triplets) {
if (this.constructor !== Cycle) {
return new Cycle(triplets);
}
var i, triplet;
// Cycle for the Sprite image.
// A cycle is a list of triplet (x offset, y offset, game tick duration)
this.triplets = triplets;
// total duration of the animation in ticks
this.cycleDuration = 0;
// this array knows on which ticks in the animation
// an image change is needed
this.changingTicks = triplets.map(function(triplet) {
this.cycleDuration += triplet[2];
return this.cycleDuration;
}, this);
this.changingTicks.unshift(0);
this.currentTripletIndex = undefined;
// suppose to be private
this.sprites = [];
// if set to false, the animation will stop automaticaly after one run
this.repeat = true;
this.tick = 0;
this.done = false;
this.id = ++nb_cycle;
};
Cycle.prototype.addSprite = function addSprite(sprite) {
this.sprites.push(sprite);
sprite.cycle = this;
return this;
};
Cycle.prototype.toString = function () {
return "Cycle(" + String(this.id) + ")";
};
Cycle.prototype.update = function update() {
var sprites = this.sprites, i, sp;
for (i = 0; sp = sprites[i]; i++) {
sp.update();
}
return this;
};
Cycle.prototype.addSprites = function addSprites(sprites) {
this.sprites = this.sprites.concat(sprites);
var j, sp;
for (j = 0; sp = sprites[j]; j++) {
sp.cycle = this;
}
return this;
};
Cycle.prototype.removeSprite = function removeSprite(sprite) {
var j, sp;
for (j = 0; sp = this.sprites[j]; j++) {
if (sprite == sp) {
sp.cycle = null;
this.sprites.splice(j, 1);
}
}
return this;
};
Cycle.prototype.next = function (ticks, update) {
if (this.tick > this.cycleDuration) {
if (this.repeat)
this.tick = 0;
else {
this.done = true;
return this;
}
}
// search the current triplet index
var currentTripletIndex, i, j, sprite, next;
for (i = 0; i < this.changingTicks.length; i++) {
next = this.changingTicks[i+1];
if (this.tick >= this.changingTicks[i]) {
if(next === undefined) {
currentTripletIndex = 0;
this.tick = 0;
break;
} else if(this.tick < next) {
currentTripletIndex = i;
break;
}
}
}
if (currentTripletIndex !== undefined && currentTripletIndex !== this.currentTripletIndex) {
this.sprites.map(function(sprite) {
sprite.setXOffset(this.triplets[currentTripletIndex][0]);
sprite.setYOffset(this.triplets[currentTripletIndex][1]);
if (update) {
sprite.update();
}
}.bind(this));
this.currentTripletIndex = currentTripletIndex;
}
ticks = ticks || 1; // default tick: 1
this.tick = this.tick + ticks;
return this;
};
Cycle.prototype.reset = function resetCycle(update) {
var j, sprite;
this.tick = 0;
this.done = false;
for (j = 0; sprite = this.sprites[j]; j++) {
sprite.setXOffset(this.triplets[0][0]);
sprite.setYOffset(this.triplets[0][1]);
if (update)
sprite.update();
}
return this;
};
Cycle.prototype.go = function gotoCycle(n) {
var j, sprite;
for (j = 0; sprite = this.sprites[j]; j++) {
sprite.setXOffset(this.triplets[n][0]);
sprite.setYOffset(this.triplets[n][1]);
}
return this;
};
Ticker_ = function Ticker_(scene, paint, options) {
// backward compatiblity from the 1.1.1 API
if (typeof paint == "number") {
var buf = paint;
paint = options;
options = {tickDuration: buf}
}
this.scene = scene;
if (this.constructor !== Ticker_){
return new Ticker_(tickDuration, paint);
}
this.tickDuration = optionValue(options, 'tickDuration', 16);
this.expectedFps = 1000 / this.tickDuration;
this.useAnimationFrame = optionValue(options, 'useAnimationFrame', false);
if (!sjs.requestAnimationFrame || !sjs.cancelAnimationFrame) {
this.useAnimationFrame = false;
}
this.paint = paint;
var that = this;
this.bindedRun = function bindedRun(t) {that.run(t);}
this.start = new Date().getTime();
this.now = this.start;
this.ticksElapsed = 0;
// absolute number of ticks that have been played ever
this.currentTick = 0;
this.ticksSinceLastStart = 0;
this.droppedFrames = 0;
// will divide the framerate by 2 if true
this.lowFrameRate = false;
};
Ticker_.prototype.next = function (timestamp) {
var now = new Date().getTime();
this.diff = now - this.now;
this.now = now;
// number of ticks that have elapsed since the last start
this.lastTicksElapsed = Math.round(this.diff / this.tickDuration);
this.droppedFrames += Math.max(0, this.lastTicksElapsed - 1);
this.ticksSinceLastStart += this.lastTicksElapsed;
// add the diff to the current ticks
this.currentTick += this.lastTicksElapsed;
return this.lastTicksElapsed;
};
Ticker_.prototype.run = function(timestamp) {
if (this.paused) {
return;
}
/*if(this.lowFrameRate || this.load > 20 && this.fps < (this.expectedFps / 2)) {
this.lowFrameRate = true;
if(this.skippedFrames == 1) {
this.skippedFrames = 0;
this.skipPaint = true;
this.scene.disableUpdate = true;
} else {
this.skippedFrames = 1;
this.skipPaint = false;
this.scene.disableUpdate = false;
}
} else {
this.skipPaint = false;
}*/
var t = this;
var ticksElapsed = this.next(timestamp);
// no update needed, this happen on the first run
/*if (ticksElapsed == 0) {
// this is not a cheap operation
setTimeout(this.bindedRun, this.tickDuration);
return;
}*/
//if(!this.skipPaint) {
for (var name in this.scene.layers) {
var layer = this.scene.layers[name];
if (layer.useCanvas && layer.autoClear) {
layer.clear();
}
}
//}
this.paint(this);
// reset the keyboard change
if (this.scene.input) {
this.scene.input.next();
}
this.timeToPaint = (new Date().getTime()) - this.now;
// spread the load value on 2 frames so the value is more stable
this.load = ((this.timeToPaint / this.tickDuration * 100) + this.load) >> 1;
this.fps = Math.round(1000 / (this.now - (this.lastPaintAt || 0)));
this.lastPaintAt = this.now;
if (this.useAnimationFrame) {
this.tickDuration = 16;
this.animationId = global[sjs.requestAnimationFrame](this.bindedRun);
} else {
var _nextPaint = Math.max(this.tickDuration - this.timeToPaint, 6);
this.timeout = setTimeout(this.bindedRun, _nextPaint);
}
};
Ticker_.prototype.pause = function () {
if (this.useAnimationFrame) {
global[sjs.cancelAnimationFrame](this.animationId);
} else {
global.clearTimeout(this.timeout);
}
this.paused = true;
};
Ticker_.prototype.resume = function () {
this.start = new Date().getTime();
this.ticksElapsed = 0;
this.ticksSinceLastStart = 0;
this.paused = false;
this.run();
};
var inputSingleton = false;
function Input(scene) {
if (!inputSingleton)
inputSingleton = new _Input(scene);
return inputSingleton;
};
_Input = function _Input(scene) {
if (scene)
this.dom = scene.dom;
else
this.dom = doc.body;
var that = this;
// record the current keyboard state
this.keyboard = {};
this.mouse = {position: {}, click: undefined};
// record the keyboard changes since the last call
this.keyboardChange = {};
this.mousedown = false;
that.mousepressed = false;
this.mousereleased = false;
this.keydown = false;
this.touchMoveSensibility = 20;
this.enableCustomEvents = false;
this.touchable = 'ontouchstart' in global;
this.next = function () {
if(this.disableFor)
this.disableFor = that.disableFor - 1;
this.keyboardChange = {};
this.mousepressed = false;
this.mouse.click = undefined;
this.mousereleased = false;
}
this.disableFor = 0;
this.disable = function (ticks) {
that.disableFor = ticks;
}
this.keyPressed = function (name) {
return that.keyboardChange[name] !== undefined && that.keyboardChange[name];
};
this.keyReleased = function (name) {
return that.keyboardChange[name] !== undefined && !that.keyboardChange[name];
};
this.arrows = function arrows() {
/* Return true if any arrow key is pressed */
return this.keyboard.right || this.keyboard.left || this.keyboard.up || this.keyboard.down;
};
function fireEvent(name, value) {
if(!that.enableCustomEvents)
return;
if(doc.createEvent) {
var evObj = doc.createEvent('Events');
evObj.initEvent('sjs' + name, true, true);
evObj.value = value;
that.dom.dispatchEvent(evObj);
} else if(doc.createEventObject) {
var evObj = doc.createEventObject();
evObj.value = value;
that.dom.fireEvent('onsjs' + name, evObj);
}
}
function updateKeyChange(name, val) {
fireEvent(name, val);
if(name == "space" || name == "enter") {
updateKeyChange("action", val)
}
if (that.keyboard[name] !== val) {
that.keyboard[name] = val;
that.keyboardChange[name] = val;
}
}
// this is handling WASD, and arrows keys
function updateKeyboard(e, val) {
if (e.keyCode == 40 || e.keyCode == 83) {
updateKeyChange('down', val);
}
if (e.keyCode == 38 || e.keyCode == 87) {
updateKeyChange('up', val);
}
if (e.keyCode == 39 || e.keyCode == 68) {
updateKeyChange('right', val);
}
if (e.keyCode == 37 || e.keyCode == 65) {
updateKeyChange('left', val);
}
if (e.keyCode == 32) {
updateKeyChange('space', val);
}
if (e.keyCode == 17) {
updateKeyChange('ctrl', val);
}
if (e.keyCode == 13) {
updateKeyChange('enter', val);
}
if (e.keyCode == 27) {
updateKeyChange('esc', val);
}
// 0..9, a-z
if (e.keyCode >= 48 && e.keyCode <= 90) {
var keyStr = String.fromCharCode(e.keyCode);
updateKeyChange(keyStr.toLowerCase(), val);
}
}
var listen = function (name, fct) {
_addEventListener(global, name, fct, false);
}
// Mouse like events
function clickEvent(event) {
that.mouse.click = {
x: (event.clientX - that.dom.offsetLeft) / scene.xscale,
y: (event.clientY - that.dom.offsetTop) / scene.yscale
};
}
function mouseDownEvent(event) {
that.mousedown = true;
that.mouse.down = true;
that.mousepressed = true;
// prevent unwanted browser drag and drop behavior
_preventEvent(event);
}
function mouseUpEvent(event) {
that.mousedown = false;
that.mouse.down = false;
that.mousereleased = true;
that.mouse.click = {
x: (event.clientX - that.dom.offsetLeft) / scene.xscale,
y: (event.clientY - that.dom.offsetTop) / scene.yscale
};
}
function mouseMoveEvent(event) {
that.mouse.position = {
x: (event.clientX - that.dom.offsetLeft) / scene.xscale,
y: (event.clientY - that.dom.offsetTop) / scene.yscale
};
}
function reduceTapEvent(e) {
// To simplify I ignore multiple touch events and only return the first event
if (e.touches && e.touches.length) { e = e.touches[0]; }
else if (e.changedTouches && e.changedTouches.length) { e = e.changedTouches[0];}
return e
}
if (this.touchable) {
listen("touchstart", function (e) {
e = reduceTapEvent(e);
updateKeyChange('space', true); // tap imitates space
// simulate the click
clickEvent(e);
//store initial coordinates to find out swipe directions later
that.touchStart = {"x" : e.clientX, "y": e.clientY};
});
listen("touchend", function (e) {
mouseUpEvent(e);
that.keyboard = {}
that.touchStart = null;
});
listen("touchmove", function (e) {
_preventEvent(e); // avoid scrolling the page
e = reduceTapEvent(e);
updateKeyChange('space', false); // if it moves: it is not a tap
mouseMoveEvent(e);
if (that.touchStart) {
var deltaX = e.clientX - that.touchStart.x;
var deltaY = e.clientY - that.touchStart.y;
if (deltaY < -that.touchMoveSensibility) {
updateKeyChange('up', true);
updateKeyChange('down', false);
} else if (deltaY > that.touchMoveSensibility) {
updateKeyChange('down', true);
updateKeyChange('up', false);
} else {
updateKeyChange('up', false);
updateKeyChange('down', false);
}
if (deltaX < -that.touchMoveSensibility) {
updateKeyChange('left', true);
updateKeyChange('right', false);
} else if(deltaX > that.touchMoveSensibility) {
updateKeyChange('right', true);
updateKeyChange('left', false);
} else {
updateKeyChange('left', false);
updateKeyChange('right', false);
}
}
});
listen("touchmove", function (e) {
e = reduceTapEvent(e);
mouseMoveEvent(e);
});
};
listen("mousedown", mouseDownEvent);
listen("mouseup", mouseUpEvent);
listen("click", clickEvent);
listen("mousemove", mouseMoveEvent);
listen("keydown", function (e) {
that.keydown = true;
updateKeyboard(e, true);
});
listen("keyup", function (e) {
that.keydown = false;
updateKeyboard(e, false);
});
// can be used to avoid key jamming
listen("keypress", function (e) {});
if (!sjs.debug)
listen("contextmenu", function (e) {_preventEvent(e);});
};
// Add an automatic pause to all the scenes when the user
// quit the current window.
_addEventListener(global, "blur", function (e) {
for (var i = 0; i < sjs.scenes.length; i++) {
var scene = sjs.scenes[i];
if (!scene.autoPause)
continue;
var anon = function (scene) {
Input(scene);
inputSingleton.keyboard = {};
inputSingleton.keydown = false;
inputSingleton.mousedown = false;
// create a semi transparent layer on the game
if (scene.ticker && !scene.ticker.paused) {
scene.ticker.pause();
var div = overlay(0, 0, scene.w, scene.h);
div.innerHTML = '<h1>Paused</h1><p>Click or press any key to resume.</p>';
div.style.textAlign = 'center';
div.style.paddingTop = ((scene.h / 2) - 32) + 'px';
var listener = function (e) {
_preventEvent(e);
scene.dom.removeChild(div);
_removeEventListener(doc, 'click', listener, false);
_removeEventListener(doc, 'keyup', listener, false);
scene.ticker.resume();
}
_addEventListener(doc, 'click', listener, false);
_addEventListener(doc, 'keyup', listener, false);
scene.dom.appendChild(div);
}
}
anon(scene);
}
}, false);
Layer = function Layer(scene, name, options) {
var domElement, needToCreate, domH, domW;
if (!this || this.constructor !== Layer)
return new Layer(scene, name, options);
this.sprites = {};
this.scene = scene;
if (options === undefined)
options = {useCanvas: scene.useCanvas, autoClear: true}
if (options.useWebGL)
options.useCanvas = true;
if (options.autoClear === undefined)
this.autoClear = true;
else
this.autoClear = options.autoClear;
if (options.useCanvas === undefined)
this.useCanvas = this.scene.useCanvas;
else
this.useCanvas = options.useCanvas;
this.useWebGL = options.useWebGL;
this.name = name;
if (this.scene.layers[name] === undefined) {
this.scene.layers[name] = this;
} else {
if (sjs.debug) {
sjs.warning("A layer named " + name + " already exist.");
}
// if the user try to create a Layer that already exists,
// we send back the same.
return this.scene.layers[name];
}
this.lastZIndex = 0;
domElement = doc.getElementById(name);
if (!domElement)
needToCreate = true;
else
needToCreate = false;
if (this.useCanvas) {
if (domElement && domElement.nodeName.toLowerCase() !== "canvas") {
sjs.error("Cannot use HTMLElement " + domElement.nodeName + " with canvas renderer.");
}
if (needToCreate) {
domElement = doc.createElement('canvas');
}
} else {
if (needToCreate) {
domElement = doc.createElement('div');
}
}
if (!needToCreate) {
domH = domElement.height || domElement.style.height;
domW = domElement.width || domElement.style.width;
} else {
domH = false;
domW = false;
}
if (options.parent)
this.parent = options.parent;
else
this.parent = this.scene.dom;
this.parent.appendChild(domElement);
domElement.id = domElement.id || 'sjs' + scene.id + '-' + name;
if (!options.disableAutoZIndex) {
zindex += 1;
domElement.style.zIndex = String(zindex);
}
domElement.style.backgroundColor = options.color || domElement.style.backgroundColor;
this.h = options.h || domH || scene.h;
this.w = options.w || domW || scene.w;
if (domElement.nodeName == "CANVAS") {
domElement.height = this.h;
domElement.width = this.w;
} else {
domElement.style.height = this.h + 'px';
domElement.style.width = this.w +'px';
};
domElement.style.position = 'absolute';
domElement.style.top = domElement.style.top || '0px';
domElement.style.left = domElement.style.left || '0px';
this.dom = domElement;
// webgl needs to be set after the size
if (this.useCanvas) {
if (options.useWebGL) {
this.ctx = webgl.init(domElement);
} else {
this.ctx = domElement.getContext('2d');
}
}
};
Layer.prototype.constructor = Layer;
Layer.prototype.clear = function clear() {
if (this.useWebGL)
this.ctx.clear(this.ctx.COLOR_BUFFER_BIT | this.ctx.DEPTH_BUFFER_BIT);
else
this.ctx.clearRect(0, 0, this.dom.width, this.dom.height);
};
Layer.prototype.Sprite = function (src, options) {
if (options)
options.layer = this;
else
options = this;
return new Sprite(this.scene, src, options);
};
Layer.prototype.remove = function remove() {
this.parent.removeChild(this.dom);
delete this.scene.layers[this.name];
};
Layer.prototype.addSprite = function addSprite(sprite) {
var index = Math.random() * 11;
this.sprites[index] = sprite;
return index
};
Layer.prototype.setColor = function setColor(color) {
this.dom.style.backgroundColor = color;
};
Layer.prototype.onTop = function onTop(color) {
zindex += 1;
this.dom.style.zIndex = String(zindex);
};
List = function List(list) {
if (this.constructor !== List)
return new List(list);
// ensure that a List can be initialized with a list.
this.list = (list && (list.list || list)) || [];
this.length = this.list.length;
this.index = -1;
};
List.prototype.add = function add(sprite) {
if (sprite.length)
this.list.push.apply(this.list, sprite);
else
this.list.push(sprite);
this.length = this.list.length;
};
// alias
List.prototype.append = List.prototype.add;
List.prototype.remove = function remove(toRemove) {
var removed = false
for (var i = 0, el; el = this.list[i]; i++) {
if (el == toRemove) {
this.list.splice(i, 1);
// delete during the iteration is possible
if (this.index > -1)
this.index = this.index - 1;
i--;
removed = true;
}
}
this.length = this.list.length;
return removed;
};
List.prototype.iterate = function iterate() {
this.index += 1;
if (this.index >= this.list.length) {
this.index = -1;
return false;
}
return this.list[this.index];
};
List.prototype.pop = function pop() {
this.length -= 1;
return this.list.pop();
};
List.prototype.shift = function shift() {
this.index -= 1;
this.length -= 1;
return this.list.shift();
};
List.prototype.isIn = function isInList(el) {
for(var i=0; i<this.list.length; i++) {
if(this.list[i] == el) {
return true;
}
}
return false;
}
List.prototype.filter = function filterList(name, value) {
var newList = new List();
for(var i=0; i<this.list.length; i++) {
if(this.list[i][name] == value) {
newList.add(this.list[i]);
}
}
return newList;
}
List.prototype.empty = function () {
this.list = [];
this.length = 0;
this.index = -1;
}
var log_output = null;
function output(value) {
if(!log_output) {
log_output = doc.createElement('textarea');
log_output.style.height = "200px";
log_output.style.width = (window.innerWidth - 100) + 'px';
doc.body.appendChild(log_output);
}
log_output.value = log_output.value + value;
log_output.scrollTop = log_output.scrollHeight;
}
function _log() {
if(!sjs.debug)
return;
var result = "";
for (var i = 0; i < arguments.length; i++) {
result += arguments[i] + ' ';
}
output(result + "\r\n");
}
var sjs = {
// a global cache to load each sprite only one time.
spriteCache: {},
debug: false,
Cycle: Cycle,
Input: Input,
Scene: Scene,
SpriteList: List, // backward compatibility 1.1.1
List: List,
Sprite: Sprite,
overlay: overlay,
scenes: [],
error: function error(msg) {_log("Error: " + msg);},
warning:function warning(msg) {_log("Warning: " + msg);},
log:_log,
createEvent: null,
math: {hypo: hypo, mod: mod, normalVector: normalVector, lineSide: lineSide}
};
global.sjs = sjs;
})(this);