@@ -1,3 +1,30 @@
const SOURCE_OVER = "source-over";
const SOURCE_IN = "source-in";
const SOURCE_OUT = "source-out";
const SOURCE_ATOP = "source-atop";
const DESTINATION_OVER = "destination-over";
const DESTINATION_IN = "destination-in";
const DESTINATION_OUT = "destination-out";
const DESTINATION_ATOP = "destination-atop";
const LIGHTER = "lighter";
const COPY = "copy";
const XOR = "xor";
const MULTIPLY = "multiply";
const SCREEN = "screen";
const OVERLAY = "overlay";
const DARKEN = "darken";
const LIGHTEN = "lighten";
const COLOR_DODGE = "color-dodge";
const COLOR_BURN = "color-burn";
const HARD_LIGHT = "hard-light";
const SOFT_LIGHT = "soft-light";
const DIFFERENCE = "difference";
const EXCLUSION = "exclusion";
const HUE = "hue";
const SATURATION = "saturation";
const COLOR = "color";
const LUMINOSITY = "luminosity";

/**
* The base class for display objects. Sprite is an observable, physical body with coordinates and size
* @class Sprite
@@ -6,28 +33,38 @@
* @param {Number} width=0 [description]
* @param {Number} height=0 [description]
*/
export default class Sprite {
class Sprite {
constructor (x=0, y=0, width=0, height=0) {
this._x = x;
this._y = y;
this._pivotX = 0;
this._pivotY = 0;
this._width = width;
this._height = height;
this._sx = 1;
this._sy = 1;
this._rotation = 0;
this._opacity = 1;
this._composite = "source-over";
this._visible = true;
this._tweens = [];
// used to safely cache expensive operations. Set to true on each `set` and cleared after each `render`
this._dirty = true;

this._uuid = Sprite.uuidCount++;
}

/**
* @method Sprite#addTween
* @param {Tween} tween A new tween instance
*/
addTween (tween) {
this.tweens.push(tween);
this._tweens.push(tween);
}

centerPivot () {
this._pivotX = (this._width / 2) * this._sx;
this._pivotY = (this._height / 2) * this._sy;
}

getBoundingBox () {
@@ -44,21 +81,45 @@ export default class Sprite {
this._y += typeof y === "number" ? y : 0;
}

render () {
this._dirty = false;
render (context) {
if (this.opacity !== 1) {
context.globalAlpha = this.opacity;
}

if (this.composite !== "source-over") {
context.globalCompositeOperation = this.opacity;
}

let px = Math.floor(this._pivotX);
let py = Math.floor(this._pivotY);

context.translate(Math.floor(this._x), Math.floor(this._y));
context.translate(px, py);

if (this.sx !== 1 || this.sy !== 1) {
context.scale(this.sx, this.sy);
}

if (this.rotation !== 0) {
context.rotate(this.rotation);
}

context.translate(-px, -py);
}

update () {
for (let i = 0, len = this._tweens.length; i < len; i++) {
let tween = this._tweens[i];
tween.update();
tween.update(this);

if (tween.isComplete()) {
this._tweens.splice(i, 1);
}
}
}

get composite() { return this._composite; }

get opacity() { return this._opacity; }

get x() { return this._x; }
@@ -77,6 +138,45 @@ export default class Sprite {

get visible() { return this._visible; }

get uuid() { return this._uuid; }

set composite(val) {
switch (val) {
case SOURCE_OVER:
case SOURCE_IN:
case SOURCE_OUT:
case SOURCE_ATOP:
case DESTINATION_OVER:
case DESTINATION_IN:
case DESTINATION_OUT:
case DESTINATION_ATOP:
case LIGHTER:
case COPY:
case XOR:
case MULTIPLY:
case SCREEN:
case OVERLAY:
case DARKEN:
case LIGHTEN:
case COLOR_DODGE:
case COLOR_BURN:
case HARD_LIGHT:
case SOFT_LIGHT:
case DIFFERENCE:
case EXCLUSION:
case HUE:
case SATURATION:
case COLOR:
case LUMINOSITY:
this._composite = val;
break;
default:
throw new Error("Sprite#set composite: value must be one of globalCompositeOperation enum.");
}

this._dirty = true;
}

set opacity(val) {
this._opacity = val;
this._dirty = true;
@@ -92,6 +192,16 @@ export default class Sprite {
this._dirty = true;
}

set pivotX(val) {
this._pivotX = val;
this._dirty = true;
}

set pivotY(val) {
this._pivotY = val;
this._dirty = true;
}

set width(val) {
this._width = val;
this._dirty = true;
@@ -121,4 +231,8 @@ export default class Sprite {
this._visible = val;
this._dirty = true;
}
}
}

Sprite.uuidCount = 0;

export default Sprite;
@@ -53,7 +53,7 @@ export default class Animation {
* @method Animation#update
* @returns {null|Object} If animation not playing returns null else returns object containing current srcX/Y values
*/
update() {
update(factor) {
if (!this._playing) {
return null;
}
@@ -8,7 +8,7 @@ import Sprite from "../Sprite";
* @author Chris Peters
*/
export default class Bitmap extends Sprite {
constructor (x, y, width, height) {
constructor (x, y, width, height, image) {
super(x, y, width, height);

this._srcX = 0;
@@ -19,6 +19,10 @@ export default class Bitmap extends Sprite {
this._image = null;
this._tiling = "no-repeat";
this._animations = {};

if (image) {
this.image = image;
}
}

/**
@@ -34,9 +38,9 @@ export default class Bitmap extends Sprite {
* @method Bitmap#playAnimation
* @param {String} name The name of the animation to play
*/
playAnimation (name) {
playAnimation (name, loop) {
this._playingAnimation = name;
this._animations[name].play();
this._animations[name].play(loop);
}

/**
@@ -56,18 +60,18 @@ export default class Bitmap extends Sprite {
if (!this._imageLoaded) {
return;
}

context.save();

if (this._tiling != "no-repeat") {
super.render(context);

if (this._tiling !== "no-repeat") {
if (this._dirty) {
this._pattern = context.createPattern(this._image, this._tiling);
}

context.rect(
this._x, this._y,
this._width * this._scaleX,
this._height * this._scaleY
0, 0,
this._width * this._sx,
this._height * this._sy
);
context.fillStyle = this._pattern;
context.fill();
@@ -78,21 +82,16 @@ export default class Bitmap extends Sprite {
this._srcY,
this._srcWidth,
this._srcHeight,
this._x, this._y,
this._width * this._scaleX,
this._height * this._scaleY
0, 0,
this._width * this._sx,
this._height * this._sy
);
}

context.restore();

// reset dirty
super.render();
}

update () {
update (factor) {
if (this._playingAnimation) {
const { srcX, srcY } = this._animations[this._playingAnimation].update(ticks);
const { srcX, srcY } = this._animations[this._playingAnimation].update();
this._srcX = srcX;
this._srcY = srcY;
}
@@ -106,25 +105,24 @@ export default class Bitmap extends Sprite {
* @return {Bitmap}
*/
set image(path) {
var image = new Image();

image.onload = ()=> {
this._image = image;
let self = this;
this._image = new Image();

if (!this._srcWidth && !this._srcHeight) {
this._srcWidth = this._image.width;
this._srcHeight = this._image.height;
this._image.onload = ()=> {
if (!self._srcWidth && !self._srcHeight) {
self._srcWidth = self._image.width;
self._srcHeight = self._image.height;
}

if (!this._width && !this._height) {
this._width = this._image.width;
this._height = this._image.height;
if (!self._width && !self._height) {
self._width = self._image.width;
self._height = self._image.height;
}

this._imageLoaded = true;
self._imageLoaded = true;
};

image.src = path;
this._image.src = path;

this._dirty = true;
}
@@ -143,6 +141,7 @@ export default class Bitmap extends Sprite {
case "repeat-y":
case "no-repeat":
this._tiling = val;
break;
default:
throw new Error(
"Bitmap#setTiling: argument must be either \"repeat\", \"repeat-x\", \"repeat-y\", or \"no-repeat\"."
@@ -0,0 +1,249 @@
'use strict';

// t: current time, b: beginning value, _c: final value, d: total duration
var tweenFunctions = {
linear: function(t, b, _c, d) {
var c = _c - b;
return c * t / d + b;
},
easeInQuad: function(t, b, _c, d) {
var c = _c - b;
return c * (t /= d) * t + b;
},
easeOutQuad: function(t, b, _c, d) {
var c = _c - b;
return -c * (t /= d) * (t - 2) + b;
},
easeInOutQuad: function(t, b, _c, d) {
var c = _c - b;
if ((t /= d / 2) < 1) {
return c / 2 * t * t + b;
} else {
return -c / 2 * ((--t) * (t - 2) - 1) + b;
}
},
easeInCubic: function(t, b, _c, d) {
var c = _c - b;
return c * (t /= d) * t * t + b;
},
easeOutCubic: function(t, b, _c, d) {
var c = _c - b;
return c * ((t = t / d - 1) * t * t + 1) + b;
},
easeInOutCubic: function(t, b, _c, d) {
var c = _c - b;
if ((t /= d / 2) < 1) {
return c / 2 * t * t * t + b;
} else {
return c / 2 * ((t -= 2) * t * t + 2) + b;
}
},
easeInQuart: function(t, b, _c, d) {
var c = _c - b;
return c * (t /= d) * t * t * t + b;
},
easeOutQuart: function(t, b, _c, d) {
var c = _c - b;
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
},
easeInOutQuart: function(t, b, _c, d) {
var c = _c - b;
if ((t /= d / 2) < 1) {
return c / 2 * t * t * t * t + b;
} else {
return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
}
},
easeInQuint: function(t, b, _c, d) {
var c = _c - b;
return c * (t /= d) * t * t * t * t + b;
},
easeOutQuint: function(t, b, _c, d) {
var c = _c - b;
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
},
easeInOutQuint: function(t, b, _c, d) {
var c = _c - b;
if ((t /= d / 2) < 1) {
return c / 2 * t * t * t * t * t + b;
} else {
return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
}
},
easeInSine: function(t, b, _c, d) {
var c = _c - b;
return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
},
easeOutSine: function(t, b, _c, d) {
var c = _c - b;
return c * Math.sin(t / d * (Math.PI / 2)) + b;
},
easeInOutSine: function(t, b, _c, d) {
var c = _c - b;
return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
},
easeInExpo: function(t, b, _c, d) {
var c = _c - b;
return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
},
easeOutExpo: function(t, b, _c, d) {
var c = _c - b;
return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
},
easeInOutExpo: function(t, b, _c, d) {
var c = _c - b;
if (t === 0) {
return b;
}
if (t === d) {
return b + c;
}
if ((t /= d / 2) < 1) {
return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
} else {
return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
}
},
easeInCirc: function(t, b, _c, d) {
var c = _c - b;
return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
},
easeOutCirc: function(t, b, _c, d) {
var c = _c - b;
return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
},
easeInOutCirc: function(t, b, _c, d) {
var c = _c - b;
if ((t /= d / 2) < 1) {
return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
} else {
return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
}
},
easeInElastic: function(t, b, _c, d) {
var c = _c - b;
var a, p, s;
s = 1.70158;
p = 0;
a = c;
if (t === 0) {
return b;
} else if ((t /= d) === 1) {
return b + c;
}
if (!p) {
p = d * 0.3;
}
if (a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = p / (2 * Math.PI) * Math.asin(c / a);
}
return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
},
easeOutElastic: function(t, b, _c, d) {
var c = _c - b;
var a, p, s;
s = 1.70158;
p = 0;
a = c;
if (t === 0) {
return b;
} else if ((t /= d) === 1) {
return b + c;
}
if (!p) {
p = d * 0.3;
}
if (a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = p / (2 * Math.PI) * Math.asin(c / a);
}
return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
},
easeInOutElastic: function(t, b, _c, d) {
var c = _c - b;
var a, p, s;
s = 1.70158;
p = 0;
a = c;
if (t === 0) {
return b;
} else if ((t /= d / 2) === 2) {
return b + c;
}
if (!p) {
p = d * (0.3 * 1.5);
}
if (a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = p / (2 * Math.PI) * Math.asin(c / a);
}
if (t < 1) {
return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
} else {
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b;
}
},
easeInBack: function(t, b, _c, d, s) {
var c = _c - b;
if (s === void 0) {
s = 1.70158;
}
return c * (t /= d) * t * ((s + 1) * t - s) + b;
},
easeOutBack: function(t, b, _c, d, s) {
var c = _c - b;
if (s === void 0) {
s = 1.70158;
}
return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
},
easeInOutBack: function(t, b, _c, d, s) {
var c = _c - b;
if (s === void 0) {
s = 1.70158;
}
if ((t /= d / 2) < 1) {
return c / 2 * (t * t * (((s *= 1.525) + 1) * t - s)) + b;
} else {
return c / 2 * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b;
}
},
easeInBounce: function(t, b, _c, d) {
var c = _c - b;
var v;
v = tweenFunctions.easeOutBounce(d - t, 0, c, d);
return c - v + b;
},
easeOutBounce: function(t, b, _c, d) {
var c = _c - b;
if ((t /= d) < 1 / 2.75) {
return c * (7.5625 * t * t) + b;
} else if (t < 2 / 2.75) {
return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b;
} else if (t < 2.5 / 2.75) {
return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b;
} else {
return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b;
}
},
easeInOutBounce: function(t, b, _c, d) {
var c = _c - b;
var v;
if (t < d / 2) {
v = tweenFunctions.easeInBounce(t * 2, 0, c, d);
return v * 0.5 + b;
} else {
v = tweenFunctions.easeOutBounce(t * 2 - d, 0, c, d);
return v * 0.5 + c * 0.5 + b;
}
}
};

export default tweenFunctions;
@@ -23,28 +23,19 @@ export default class Rectangle extends Sprite {
* @memberof shape
* @method Rectangle#render
* @param {Object} context The CanvasRenderingContext2D object
* @param {Integer} factor The 0-1-based model of elapsed time
* @param {Integer} ticks Total elapsed ticks
*/
render (context, factor, ticks) {
context.save();
const x = Math.floor(this._x);
const y = Math.floor(this._y);
render (context) {
super.render(context);

if (this._fill) {
context.fillStyle = this._fill;
context.fillRect(x, y, this._width, this._height);
context.fillRect(0, 0, this._width, this._height);
}

if (this._stroke) {
context.strokeStyle = this._stroke;
context.strokeRect(x, y, this._width, this._height);
context.strokeRect(0, 0, this._width, this._height);
}

context.restore();

// reset dirty
super.render();
}

/**
@@ -20,6 +20,17 @@ export default class Text extends Sprite {
this._fill = "#000";
this._stroke = "";
this._metrics = null;

this._callWhenMetricsAvailable = [];
}

centerPivot () {
if (this._metrics) {
this._pivotX = (this._metrics.width / 2) * this._sx;
this._pivotY = (this._size / 2) * this._sy;
} else {
this._callWhenMetricsAvailable.push("centerPivot");
}
}

getBoundingBox () {
@@ -35,29 +46,33 @@ export default class Text extends Sprite {
* @memberof text
* @method Text#render
* @param {Object} context The CanvasRenderingContext2D object
* @param {Integer} factor The 0-1-based model of elapsed time
* @param {Integer} ticks Total elapsed ticks
*/
render (context) {
super.render(context);

if (this._dirty) {
this._metrics = context.measureText(this._value);

for (let fn of this._callWhenMetricsAvailable) {
this[fn]();
}

// reset!
this._callWhenMetricsAvailable = [];
}

context.font = `${this._size}px ${this._font}`;
context.textBaseline = this._baseline;

if (this._fill) {
context.fillStyle = this._fill;
context.fillText(this._value, this._x, this._y);
context.fillText(this._value, 0, 0);
}

if (this._stroke) {
context.strokeStyle = this._stroke;
context.strokeText(this._value, this._x, this._y);
}

if (this._dirty) {
this._metrics = context.measureText(this._value);
context.strokeText(this._value, 0, 0);
}

// reset dirty
super.render();
}

get value () { return this._value; }
@@ -88,11 +88,9 @@ export default class TextInput extends Text {
* @memberof text
* @method TextInput#render
* @param {Object} context The CanvasRenderingContext2D object
* @param {Integer} factor The 0-1-based model of elapsed time
* @param {Integer} ticks Total elapsed ticks
*/
render(context, factor, tick) {
super.render(context, factor, tick);
render(context) {
super.render(context);

if (tick - this._lastTick >= this._blinkFrames) {
this._lastTick = tick;
@@ -103,13 +101,11 @@ export default class TextInput extends Text {

if (this._caretShow && this._focused) {
context.save();
this._rect.setX(textMeasurement.width + 1);
this._rect.setHeight(this._size).setWidth(this._size / 4);
this._rect.x = textMeasurement.width;
this._rect.height = this._size
this._rect.width = this._size / 4;
this._rect.render(context);
context.restore();
}

// reset dirty
super.render();
}
}
@@ -15,14 +15,14 @@ import easing from "../lib/easing";
this._ms = ms;
this._easing = easing;

this._currentFrame = null;
this._currentFrame = 0;
this._startFrame = 1;
this._endFrame = null;
this._totalFrames = this._ms / (1000 / 60);
this._totalFrames = Math.round( this._ms / (1000 / 60) );
}

isComplete() {
return this._currentFrame < this._endFrame;
return this._currentFrame >= this._endFrame;
}

onComplete() {
@@ -40,8 +40,8 @@ import easing from "../lib/easing";

entity[actualProp] = easing[this._easing](
this._currentFrame,
this._from[prop],
this._to[prop] - this._from[prop],
this._from[actualProp],
this._to[actualProp],
this._totalFrames
);
}