/*! canvallax v2.0.0 ( built 2016-08-05 ) https://github.com/shshaw/Canvallax.js @preserve */
(function(win){
'use strict';
var /**
* Canvallax object containing all classes & methods
* @namespace {object} canvallax
* @public
*/
canvallax = win.canvallax = win.canvallax || {},
doc = document,
root = doc.documentElement,
body = doc.body,
arr = Array.prototype,
// requestAnimationFrame polyfill
requestAnimationFrame = win.requestAnimationFrame || win.webkitRequestAnimationFrame || win.mozRequestAnimationFrame || win.msRequestAnimationFrame || function(callback){ win.setTimeout(callback, 20); }; // IE Fallback
// Exit if browser does not support canvas
if ( !win.CanvasRenderingContext2D ) { return false; }
// Shorthand name
win.clx = canvallax;
var rad = Math.PI / 180,
twoPI = 2 * Math.PI,
noop = function(){},
fnType = typeof noop,
isFunction = function(fn){ return typeof fn === fnType; };
////////////////////////////////////////
/**
* Extend an object with properties from other objects.
*
* If only one argument is provided, the `target` is assumed to be `this` in the current context.
*
* @type method
* @memberof canvallax
*
* @param {object} target - Target object to receive properties from the other objects.
* @param {...object} - Objects to merge
*
* @returns {object} - Object with merged properties
*/
function extend() {
var a = arguments,
target = a[0] || {},
length = a.length,
i = 1,
key;
if ( length === 1 ) {
target = this;
i = 0;
}
for ( ; i < length; i++ ) {
if ( a[i] ) {
for ( key in a[i] ) {
if ( a[i].hasOwnProperty(key) ) { target[key] = a[i][key]; }
}
}
}
return target;
}
canvallax.extend = extend;
////////////////////////////////////////
/**
* Create a clone of an object or Class
*
* @type method
* @memberof canvallax
*
* @param {!object} target - Original to clone. If not included, will default to `this`
* @param {!object} properties - Properties to include on the clone
* @param {!boolean} cloneChildren - If the original has children, clone them.
*
* @returns {object} - Cloned object containing extra properties from the provided object.
*/
function clone(target, properties, cloneChildren) {
if ( arguments.length <= 1 || typeof properties === "boolean" ) {
cloneChildren = properties;
properties = target;
target = this;
}
var props = extend({}, target, properties),
len = props.length,
i = 0;
/** Clone all children */
if ( len && cloneChildren ) {
props.children = [];
props.length = 0;
for ( ; i < len; i++ ) {
if ( props[i] && props[i].clone ) {
props.children[i] = props[i].clone();
delete props[i];
}
}
}
return new target.constructor(props);
}
canvallax.clone = clone;
////////////////////////////////////////
// Gets around using `.apply` for creating new instances of a class, adapted from http://stackoverflow.com/a/1608546/1012919
function construct(constructor, args) {
function C(){ return constructor.apply(this, args); }
C.prototype = constructor.prototype;
return new C();
}
/**
* Create a new Class with the properties provided
*
* @type method
* @memberof canvallax
*
* @param {!object} target - Original to clone. If not included, will default to `this`
* @param {!object} properties - Properties to include on the clone
*
* @returns {object} - Cloned object containing extra properties from the provided object.
*/
function createClass(){
function C(options) {
var me = this,
len = arguments.length,
args,
i = 0;
// Ensure object is always created as `new Class` even if `new` isn't used.
if ( !(me instanceof C) ) {
args = new Array(len);
for(; i < len; i++) { args[i] = arguments[i]; }
return construct(C,args);
}
if ( len === 1 ) { extend(me,options); }
me.fn = C.fn;
if ( me.init ) { me.init.apply(me,arguments); }
// Autoplay animation-like objects
if ( me.playing && me.play ) { me.play(); }
return me;
}
var len = arguments.length,
arg,
i = 0,
fn = {
init: noop,
extend: extend,
clone: clone
};
for( ; i < len; i++ ) {
arg = arguments[i];
// Get the prototype of classes that the new class will inherit from, if available.
if ( arg.prototype ) { arg = arg.prototype; }
for ( var key in arg ) {
if ( arg.hasOwnProperty(key) ) { fn[key] = arg[key]; }
}
}
fn.constructor = C;
C.fn = C.prototype = fn;
return C;
}
canvallax.createClass = createClass;
/**
* Array-like properties used for some Canvallax classes
*
* @mixin arrayLike
*/
var arrayLike = {
length: 0,
splice: arr.splice,
indexOf: arr.indexOf,
push: arr.push,
sort: arr.sort,
unshift: arr.unshift,
/**
* Add an element, group or array of elements to collection
*
* @param {...object|object[]} element - Element or array of elements to be added
* @returns {this}
* @memberof! arrayLike
*/
add: function(el){
var me = this,
elements = ( el && el.length > -1 && Array.isArray(el) ? el : arguments ),
len = elements.length,
i = 0;
for ( ; i < len; i++ ) {
// Prevent adding `false` or `undefined` elements
if ( elements[i] ) { me.push(elements[i]); }
}
return me;
},
/**
* Run a function for each item in collection
* @param {function} callback - Callback function run for each item
* @param thisArg - Overrride `this` in the callback function
* @returns {this}
* @memberof! arrayLike
*/
each: function(callback,thisArg){
var me = this,
length = this.length,
i = 0,
t;
for ( ; i < length; i++ ) {
t = thisArg || me[i];
if ( callback.call( t, me[ i ], i ) === false ) { break; }
}
return this;
},
/**
* Remove an element from collection
* @param {...object|object[]} element - Element or array of elements to be removed
* @returns {this}
* @memberof! arrayLike
*/
remove: function(el){
var me = this,
elements = ( el && el.length > -1 && Array.isArray(el) ? el : arguments ),
len = elements.length,
index;
while (len--) {
index = me.indexOf(elements[len]);
if ( index > -1 ) { me.splice(index, 1); }
}
return me;
}
};
var _transformAttr = ['width','height'];
/**
* Core properties used for most Canvallax objects
*
* @mixin core
*
* @property {number} x=0 - `x` coordinate, horizontal offset from the left
* @property {number} y=0 - `y` coordinate, vertical offset from the top
* @property {number} z=1 - `z` coordinate, scale relative to the parent. Affects the final rendered coordinates.
* @property {number} opacity=1 - Object's opacity with `1` as fully opaque and `0` as fully transparent, with rendering skipped. Relative to the parent's opacity.
* @property {number} scale=1 - How large the object should be rendered relative to its natural size, from the `transformOrigin` property]
* @property {number} rotation=0 - Amount of rotation in degrees from the `transformOrigin` property
*
* @borrows canvallax.extend as set
*
* @property {core.preRender} preRender - Callback before the object is rendered.
* @property {core._render} _render - Object specific callback to render to the context.
* @property {core.postRender} postRender - Callback after the object is rendered.
*
*/
var core = {
x: 0,
y: 0,
z: 1,
opacity: 1,
scale: 1,
rotation: 0,
/**
* Add object to a parent
*
* @type {function}
* @param {...object|object[]} element - Parent or array of parents for the object to be added to
* @returns {this}
* @memberof! core
*
* @example
* var scene = canvallax.Scene(),
* rect = canvallax.Rectangle();
*
* rect.addTo(scene);
*/
addTo: function(el){
var elements = ( el && el.length > -1 && Array.isArray(el) ? el : arguments ),
len = elements.length,
i = 0;
for ( ; i < len; i++ ) {
if ( elements[i] && elements[i].add ) { elements[i].add(this); }
}
return this;
},
/**
* Set multiple properties of an object
*
* @type {function}
* @param {object} - Object with properties to merge
* @returns {this}
*
* @memberof! core
*/
extend: extend,
set: extend,
/**
* Main rendering function that calls all callbacks, sets the context alpha & blend, and renders children, if any.
* @type {function}
* @returns {this}
*
* @param {CanvasRenderingContext2D} ctx - 2d canvas context
* @param {canvallax.Scene|canvallax.Group=} parent - Parent object, usually a `{@link canvallax.Scene}`
*
* @memberof! core
*/
render: function(ctx,parent) {
if ( !ctx ) { return; }
var me = this,
len = me.length,
i = 0,
pos, key, o;
parent = parent || me.parent;
ctx.save();
// Clear previous frame
if ( me.clearFrames && me.clear ) { me.clear(ctx,parent); }
// Opacity based on parent's opacity.
o = ctx.globalAlpha * me.opacity;
if ( o > 0 ) {
ctx.globalAlpha = o;
if ( me.blend ) { ctx.globalCompositeOperation = me.blend; }
// Apply clipping mask
if ( me.clip ) { me._clip(ctx,parent); }
// 'z' scaling if it has a parent and isn't fixed
if ( me.fixed || ( parent && parent.transform(ctx, me.z) ) ) {
// Apply this element's transforms. If scale is 0, the element won't continue to render.
if ( me.transform(ctx) ) {
// Pre-render callback
if ( me.preRender ) { me.preRender(ctx,parent); }
// Render this
if ( me._render ) { me._render(ctx,parent); }
// Render children
for ( ; i < len; i++ ){ me[i].render(ctx,me); }
// Post-render callback
if ( me.postRender ) { me.postRender(ctx,parent); }
}
}
}
ctx.restore();
return me;
},
/**
* Get the canvas the object is rendering onto
* @type {function}
* @memberof! core
*/
getCanvas: function(){
return this.canvas || ( this.parent ? this.parent.getCanvas() : false );
},
/**
* Where the object's transforms will occur, either as an array of coordinates or two keywords separated by a space.
*
* The default of `'center center'` means that `rotation` and `scale` transforms will be relative to the center of the object's `width` and `height`.
*
* As a string, the first keyword can be `left`, `center` or `right` cooresponding to the appropriate horizontal position, and the second keyword can be `top`, `center` or `bottom` cooresponding to the appropriate vertical position.
*
* @type {string|number[]}
* @default
* @memberof! core
*/
transformOrigin: 'center center',
/**
* Calculate the transform coordinates based on width/height.
* @private
*/
calcTransformPoint: function() {
var me = this,
point = [0,0],
origin = me.transformOrigin.split(' '),
i = 0,
val, multiplier;
for ( ; i < 2; i++) {
val = origin[i];
multiplier = (
val === 'center' ? 0.5 :
val === 'right' || val === 'bottom' ? 1 :
val.indexOf('%') ? parseFloat(val)/100 :
0
);
if ( multiplier ) { point[i] = me[_transformAttr[i]] * multiplier; }
}
return point;
},
/**
* Get the coordinates where the transforms should occur based on the transform origin.
* @private
* @type {function}
* @param {boolean} force - force an update of the coordinate cache.
* @returns {array} - Array of `x` & `y` coordinates.
* @memberof! core
*/
getTransformPoint: function(force){
var me = this,
point = me._transformPoint,
origin = me.transformOrigin,
isArr = Array.isArray(origin);
// If this is a `canvallax.Group` with a parent `canvallax.Scene` and no exact width & height or array of transformOrigin coordinates, then render relative to the parent scene's coordinates
if ( !isArr && !me.width && !me.height && me.length && me.parent ) { return me.parent.getTransformPoint(); }
// Cache values to avoid recalculation
if ( force || (!point || me._transformOrigin !== origin) ) {
point = ( isArr ? origin : me.calcTransformPoint() );
me._transformOrigin = me.transformOrigin;
me._transformPoint = point;
}
return point;
},
/**
* Returns the object's current `x` and `y` coordinates relative to the parent.
* @private
* @type {function}
* @param {number=} coordScale - Scale of the coordinates, typically the child's `z`
* @returns {array}
* @memberof! core
*/
getCoords: function(coordScale){
var x = this.x,
y = this.y,
offset = this.offset,
parent = this.parent,
parentOffset = !this.fixed && parent && parent.getCoords ? parent.getCoords(coordScale) : false;
if ( offset ) {
x += offset.x || 0;
y += offset.y || 0;
}
if ( parentOffset ) {
x += parentOffset[0];
y += parentOffset[1];
}
if ( !parent && coordScale !== undefined ) {
x *= coordScale;
y *= coordScale;
}
return [x,y];
},
/**
* Transforms the canvas context based on the object's properties.
* @private
* @type {function}
* @param {CanvasRenderingContext2D} ctx - 2d canvas context
* @param {number=} relativeZ - `z` value to base the scaling on, typically of the child.
* @returns {boolean}
* @memberof! core
*/
transform: function(ctx, relativeZ) {
var scale = ( relativeZ !== undefined ? relativeZ : this.scale ),
coords, transformPoint;
if ( scale <= 0 ) {
return false;
}
if ( scale !== 1 || (this.rotation % 360) !== 0 ) {
coords = this.getCoords(relativeZ);
transformPoint = this.getTransformPoint();
coords[0] += transformPoint[0];
coords[1] += transformPoint[1];
ctx.translate(coords[0],coords[1]);
if ( this.rotation ) { ctx.rotate(this.rotation * rad); }
ctx.scale(scale,scale);
ctx.translate(-coords[0],-coords[1]);
}
return this;
},
/**
* Element or custom function to clip object to for masking effects.
* @name clip
* @type {function|canvallax.Element}
* @memberof! core
* @example
* // circular image!
* var circle = canvallax.Ellipse({ width: 100, height: 100 }),
* image = canvallax.Image({ src: 'myimage.jpg', clip: circle });
*/
/**
* Clip to element or with custom function
* @private
* @type {function}
* @param {CanvasRenderingContext2D} ctx - 2d canvas context
* @param {canvallax.Scene|canvallax.Group} parent - Parent object, usually a `{@link canvallax.Scene}`
* @memberof! core
*/
_clip: function(ctx,parent){
var me = this;
ctx.beginPath();
if ( me.clip.render ) {
me.clip.parent = parent || me;
me.clip.render(ctx,parent);
} else {
me.clip.call(me,ctx,parent);
}
ctx.clip();
},
/**
* Create a clone of this object
* @borrows clone as clone
* @method
* @param {object=} options - Properties to be applied to the cloned object
* @memberof! core
*/
/**
* Callback function triggered when an intance is first created.
* Receives all arguments passed to the Object's creation function.
* @callback init
* @type {function}
* @memberof! core
*/
/**
* Callback before the object is rendered. Ideal for updating properties before object is drawn to canvas.
* @callback preRender
* @param {CanvasRenderingContext2D} ctx - 2d canvas context
* @param {canvallax.Scene|canvallax.Group} parent - Parent object, usually a `{@link canvallax.Scene}`
* @memberof! core
*/
/**
* Object specific rendering callback
* @callback _render
* @param {CanvasRenderingContext2D} ctx - 2d canvas context
* @param {canvallax.Scene|canvallax.Group} parent - Parent object, usually a `{@link canvallax.Scene}`
* @memberof! core
*/
/**
* Callback after the object is rendered.
* @callback postRender
* @param {CanvasRenderingContext2D} ctx - 2d canvas context
* @param {canvallax.Scene|canvallax.Group} parent - Parent object, usually a `{@link canvallax.Scene}`
* @memberof! core
*/
};
/**
* An array-like object that groups all `requestAnimationFrame` calls into one.
*/
var animations = extend({},arrayLike,{
animate: function(){
animations.frame = requestAnimationFrame(animations.animate);
var len = animations.length,
el;
if ( !animations.playing || len === 0 ) { animations.stop(); return; }
while(len--){
el = animations[len];
if ( el && el.playing && ( el.render && !el.render() )) { el.stop(); }
}
},
play: function(){
animations.frame = animations.frame || requestAnimationFrame(animations.animate);
animations.playing = true;
},
stop: function(){
animations.playing = false;
animations.frame = null;
},
kill: function(){
animations.stop();
return animations.splice(0);
}
});
canvallax.animations = animations;
/**
*
* Shared properties for classes that need to re-render every `requestAnimationFrame`.
*
* @mixin animateCore
*
*/
var animateCore = {
/**
* Play the animation by adding it to the main `requestAnimationFrame` call.
*
* @method
* @memberof! animateCore
*/
play: function(){
this.playing = true;
var index = animations.indexOf(this);
if ( index == -1 ) { animations[ ( this.animateLast ? 'unshift' : 'push' ) ](this); }
animations.play();
return this;
},
/**
* Stop the animation by removing it from the main `requestAnimationFrame` call.
*
* @method
* @memberof! animateCore
*/
stop: function(){
this.playing = false;
var index = animations.indexOf(this);
if ( index > -1 ) { animations.splice(index, 1); }
return this;
}
};
/**
* Easing functions for animations, adapted from {@link http://greweb.me/2012/02/bezier-curve-based-easing-functions-from-concept-to-implementation/}
*
* @memberof! canvallax
*
* @property {function} linear - Linear easing
* @property {function} inQuad - Quadratic easing in
* @property {function} outQuad - Quadratic easing out
* @property {function} inOutQuad - Quadratic easing in & out, the default animation easing.
* @property {function} inCubic - Cubic easing in
* @property {function} outCubic - Cubic easing out
* @property {function} inOutCubic - Cubic easing in & out
* @property {function} inQuart - Quart easing in
* @property {function} outQuart - Quart easing out
* @property {function} inOutQuart - Quart easing in & out
*
*/
canvallax.ease = {
linear: function(t){ return t; },
inQuad: function(t){ return t*t; },
outQuad: function(t){ return t*(2-t); },
inOutQuad: function(t){ return t<0.5 ? 2*t*t : -1+(4-2*t)*t; },
inCubic: function(t){ return t*t*t; },
outCubic: function(t){ return (--t)*t*t+1; },
inOutCubic: function(t){ return t<0.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1; },
inQuart: function (t){ return t*t*t*t; },
outQuart: function (t){ return 1-(--t)*t*t*t; },
inOutQuart: function(t){ return t<0.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t; }
};
/**
* Animate an object's properties to new values
* @class
* @returns {object} Animate instance
* @memberof canvallax
* @mixes animateCore
*
* @param {object} target - Object to animate.
* @param {number} duration - Duration in seconds.
* @param {object} properties - Properties & values to animate.
* @param {object} options - Animation specific options, applied to the animation as properties.
*
* @param {number} options.repeat=0 - Number of times to repeat the animation. -1 is infinite
* @param {boolean} options.yoyo - Reverse animation on repeat
* @param {function|string} options.ease=canvallax.ease.inOutQuad - Easing function to use. Many functions are built in to {@link canvallax.ease}, and you can reference them directly or use a string of the property instead of the function itself like `'inOutCubic'`
* @param {boolean} options.reversed - Play animation backwards
* @param {canvallax.Animate.onStart} options.onStart - Callback for when animation starts
* @param {canvallax.Animate.onUpdate} options.onUpdate - Callback for every `requestAnimationFrame` render
* @param {canvallax.Animate.onComplete} options.onComplete - Callback for when animation completes
*
* @example
* // Make a square spin forever
* var redSquare = canvallax.Rectangle({ fill: '#F00', width: 100, height: 100 });
* canvallax.Animate(redSquare, 1, { rotation: 360 },{ ease: canvallax.ease.linear, repeat: -1 });
*
*/
function Animate(target,duration,to,opts){
if ( !(this instanceof Animate) ) { return new Animate(target,duration,to,opts); }
var me = this,
key;
extend(me,opts);
me.target = target;
me.to = to;
if ( !me.from ) {
me.from = {};
for (key in to){ me.from[key] = target[key]; }
}
me.duration(duration);
//me.render = me.render.bind(me);
me.ease = canvallax.ease[me.ease] || me.ease;
me.restart();
return me;
}
Animate.fn = Animate.prototype = extend({},animateCore, /** @lends Animate# */{
repeat: 0,
ease: canvallax.ease.inOutQuad,
/**
* Get or set the animation's duration
*
* @method
* @memberof! canvallax.Animate
*
* @param {number} Set the duration in seconds, or retreive the current duration
*
*/
duration: function(dur){
if ( dur ) { this._d = dur * 1000; }
else { return this._d / 1000; }
},
/**
* Start the animation from the beginning.
*
* @method
* @memberof! canvallax.Animate
*/
restart: function(isRepeat){
// Animation start time.
var d = ( isRepeat ? this.repeatDelay : this.delay );
this._s = Date.now() + (d ? d * 1000 : 0);
this._p = 0;
if ( this.onStart ) { this.onStart(); }
this.play();
},
pause: function(){
// Save the time the animation was paused to calculate elapsed time later.
this._p = this._p || Date.now();
this.playing = false;
},
/**
* Reverse the animation & restart.
*
* @method
* @memberof! canvallax.Animate
*/
reverse: function(){
this.reversed = !this.reversed;
this.restart();
},
/** @private */
render: function(){
var me = this,
now = Date.now(),
progress, delta, key;
if ( !me.playing ) { return; }
// If animation was paused, add the elapsed time to the start time.
if ( me._p ) {
me._s += ( now - me._p );
me._p = 0;
}
if ( now < me._s ) { return true; }
progress = ( now - me._s ) / me._d;
if ( progress > 1 ) { progress = 1; }
delta = me.ease(me.reversed ? 1 - progress : progress );
for (key in me.to){
me.target[key] = me.from[key] + (me.to[key] - me.from[key]) * delta;
}
if ( me.onUpdate && me.onUpdate() === false ) { return false; }
if ( progress === 1 ) {
if ( me.onComplete ) { me.onComplete(); }
if ( me.yoyo ) { me.reversed = !me.reversed; }
if ( me.repeat === 0) {
return false;
} else {
if ( me.repeat > 0 ) { me.repeat--; }
me.restart(true);
}
}
return true;
}
});
/**
* Callback for when animation starts, triggered each time an animation repeats.
* @callback onStart
* @memberof! canvallax.Animate
*/
/**
* Callback for when animation values are updated each `requestAnimationFrame`.
*
* If this callback returns `false`, the animation will stop.
*
* Useful for rendering a {@link canvallax.Scene} only while an element is animating.
*
* @callback onUpdate
* @memberof! canvallax.Animate
* @returns {boolean?}
*
* @example
* // Make a square spin
* var scene = canvallax.Scene({ playing: false }), // Scene will not automatically render
* redSquare = canvallax.Rectangle({ fill: '#F00', width: 100, height: 100 });
* canvallax.Animate(redSquare, 1, { rotation: 360 },{ onUpdate: scene.render }); // Only update the scene while element is rendering
*
*/
/**
* Callback for when animation reaches the end, triggered each time an animation repeats.
*
* @callback onComplete
* @memberof! canvallax.Animate
*/
canvallax.Animate = Animate;
/**
* Animate an object's properties to new values
*
* @class canvallax.Animate.from
* @memberof canvallax.Animate
*
* @param {object} target - Object to animate
* @param {number} duration - Duration in seconds
* @param {object} fromProperties - Properties & values to animate from
* @param {object} options - Animation specific options
*
*/
Animate.from = function(target,duration,from,options){
var to = {};
for (var key in from){ to[key] = target[key]; }
options = options || {};
options.from = from;
return new Animate(target,duration,to,options);
};
/**
* Animate an object's properties to new values
*
* @class canvallax.Animate.fromTo
* @memberof canvallax.Animate
*
* @param {object} target - Object to animate
* @param {number} duration - Duration in seconds
* @param {object} fromProperties - Properties & values to animate from
* @param {object} toProperties - Properties & values to animate to
* @param {object} options - Animation specific options
*
*/
Animate.fromTo = function(target,duration,from,to,options){
options = options || {};
options.from = from;
return new Animate(target,duration,to,options);
};
/**
* Animate an object's properties to new values. See {@link canvallax.Animate} for more details.
*
* @method to
* @extends canvallax.Animate
* @memberof! core
*
* @param {number} duration - Duration in seconds
* @param {object} toProperties - Properties & values to animate to
* @param {object} options - [Animation specific options]{@link canvallax.Animate}
*
* @example
* // Make a square spin forever
* var redSquare = canvallax.Rectangle({ fill: '#F00', width: 100, height: 100 });
* redSquare.to(1, { rotation: 360 },{ ease: canvallax.ease.linear, repeat: -1 });
*
*/
core.to = core.animate = function(d,t,o){ return new Animate(this,d,t,o); };
/**
* Animate an object's properties from values. See {@link canvallax.Animate.from} for more details.
*
* @method from
* @borrows canvallax.Animate.from
* @memberof! core
*
* @param {number} duration - Duration in seconds
* @param {object} fromProperties - Properties & values to animate from
* @param {object} options - [Animation specific options]{@link canvallax.Animate}
*
*/
core.from = function(d,f,o){ return new Animate.from(this,d,f,o); };
/**
* Animate an object's properties from values to another set of values. See {@link canvallax.Animate.fromTo} for more details.
*
* @method fromTo
* @extends canvallax.Animate.fromTo
* @memberof! core
* @instance
*
* @param {number} duration - Duration in seconds
* @param {object} fromProperties - Properties & values to animate from
* @param {object} toProperties - Properties & values to animate to
* @param {object} options - [Animation specific options]{@link canvallax.Animate}
*
*/
core.fromTo = function(d,f,t,o){ return new Animate.fromTo(this,d,f,t,o); };
function zIndexSort(a,b){
var sort = ( a.zIndex === b.zIndex ? 0 : a.zIndex < b.zIndex ? -1 : 1 );
return sort || ( a.z === b.z ? 0 : a.z < b.z ? -1 : 1 );
}
/**
* Control a group of element's positioning and transforms together
*
* @class An array-like collection of canvallax elements.
* @mixes core
* @memberof canvallax
*
*/
canvallax.Group = createClass(core,arrayLike,
/** @lends canvallax.Group# */
{
type: 'group',
/**
* Add an element, group or array of elements to collection
*
* @param {...object|object[]} element - Element or array of elements to be added
* @returns {this}
*/
add: function(el){
var me = this,
elements = ( el && el.length > -1 && Array.isArray(el) ? el : arguments ),
len = elements.length,
i = 0;
for ( ; i < len; i++ ) {
if ( elements[i] ) { // Prevent adding `false` or `undefined` elements
elements[i].parent = me;
me.push(elements[i]);
}
}
return me.sort(zIndexSort);
},
init: function(options){
if ( options && options.children ) { this.add(options.children); }
}
});
/**
* Canvallax Scenes are where elements are rendered, essentially a fancy wrapper for a `<canvas>` element.
*
* If you're using the default `fullscreen` setup, then you probably want these styles:
*
* ```css
* .canvallax {
* position: fixed;
* top: 0;
* left: 0;
* z-index: -1;
* }
* ```
*
* @class
* @mixes core|animateCore
* @extends canvallax.Group
* @memberof canvallax
*
* @param {object} options - Object containing properties to be applied to the new `canvallax.Scene` instance. Reference the properties below for
*
* @property {HTMLCanvasElement} canvas=null - `<canvas>` element the scene should be rendered on. Creates a new `<canvas>` by default
* @property {node} parentElement=document.body - Node the `<canvas>` should be appended to upon initialization.
* @property {string} className=null - Classes to add to the canvas, in addition to the `'canvallax'` class automatically added.
* @property {boolean} fullscreen=true - Set the canvas width and height to the size of the window, and update on window resize.
* @property {boolean} clearFrames=true - Should the canvas be cleared before rendering?
* @property {number} width=null - Width of the `<canvas>`, set automatically if `fullscreen` is true or a `<canvas>` element is provided
* @property {number} height=null - Heightof the `<canvas>`, set automatically if `fullscreen` is true or a `<canvas>` element is provided
* @property {boolean} playing=true - If true, the scene will be re-rendered each `requestAnimationFrame`
*
* @example
* // draw scene to an existing `<canvas>`.
* var scene = canvallax.Scene({
fullscreen: false,
* canvas: document.getElementById('#myCanvas')
* });
*
*/
var styles = '<style>.canvallax--fullscreen { position: fixed; top: 0; left: 0; z-index: -1; }</style>';
canvallax.Scene = createClass(canvallax.Group,animateCore,
/** @lends canvallax.Scene# */
{
type: 'scene',
parentElement: body,
className: '',
fullscreen: true,
includeStyles: true,
playing: true,
animateLast: true,
clearFrames: true,
/**
* Function to clear the canvas context if `clearFrames` is true.
* @type {function}
* @param {CanvasRenderingContext2D} ctx - 2d canvas context
* @memberof! canvallax.Scene
*/
clear: function(ctx){
var fill = this.fill;
if ( fill ) { ctx.fillStyle = fill; }
ctx[ this.fill ? 'fillRect' : 'clearRect' ](0, 0, this.width, this.height);
return this;
},
/**
* Resize the `<canvas>`
* @type {function}
* @param {number} width
* @param {number} height
* @memberof! canvallax.Scene
*/
resize: function(width,height){
this.width = this.canvas.width = width || this.width;
this.height = this.canvas.height = height || this.height;
return this;
},
/**
* Resize the `<canvas>` to fit the full `window`
* @type {function}
* @memberof! canvallax.Scene
*/
resizeFullscreen: function() {
this.resize(win.innerWidth,win.innerHeight);
return this;
},
/**
* @param {Object} [options] Options object
* @memberof! canvallax.Scene
*/
init: function(options){
var me = this;
if ( !me.canvas ) {
me.canvas = doc.createElement('canvas');
me.parentElement.insertBefore(me.canvas, me.parentElement.firstChild);
}
me.ctx = me.canvas.getContext('2d');
me.className += ' canvallax ';
if ( me.fullscreen ) {
me.resizeFullscreen();
win.addEventListener('resize', me.resizeFullscreen.bind(me));
if ( styles && me.includeStyles ) {
doc.head.insertAdjacentHTML('afterbegin',styles);
styles = null;
}
me.className += ' canvallax--fullscreen ';
} else {
me.resize(me.width || me.canvas.width, me.height || me.canvas.height);
}
me.canvas.className += me.className;
if ( options && options.children ) { me.add(options.children); }
me.render = me.render.bind(me,me.ctx,me);
}
});
/**
* Elements are everything drawn on the Canvallax canvas. Element instances can be created either by calling `new canvallax.Element` or simply `canvallax.Element`.
*
* `canvallax.Element` is a class that doesn't do much by itself, but rather is the framework for the other elements like `{@link canvallax.Ellipse}` and `{@link canvallax.Image}`, and allows you to create your own custom elements.
*
* In order to be rendered, Canvallax Element instances need to have a `draw` function and be added to a `{@link canvallax.Scene}` or `{@link canvallax.Group}`. Most elements will also need a `fill` or `stroke` to be visible.
*
* @mixin
* @mixes core
* @memberOf canvallax
* @returns {canvallax.Element}
*
* @param {object} options - Object containing properties to be applied to the new instance. Reference the properties below.
*
* @property {canvallax.Scene|canvallax.Group} parent=null - Parent object, automatically assigned when added to a `{@link canvallax.Scene}` or `{@link canvallax.Group}`.
* @property {string} fill=null - Fill color
* @property {string} stroke=null - Stroke color
* @property {number} lineWidth=1 - Width of the stroke, if `stroke` is set.
* @property {number} zIndex=null - Stacking order of the element, higher numbers are rendered last making them appear on top of lower zIndex elements. Defaults to `z` property
* @property {boolean} fixed=null - If false, the element will be relative to parent, otherwise it will render fixed on the canvas.
*
* @example
* var scene = canvallax.Scene(),
* rect = canvallax.Rectangle(); // A type of canvallax.Element
*
* scene.add(rect);
*/
canvallax.Element = createClass(core,
/** @lends canvallax.Element# */
{
type: 'element',
lineWidth: 1,
_render: function(ctx,parent){
var me = this;
if ( me.draw ) {
ctx.beginPath();
me.draw(ctx,me.getCoords(me.z),parent);
}
if ( me.fill ) {
if ( isFunction(me.fill) ) { me.fill(ctx,parent); }
else {
ctx.fillStyle = me.fill;
ctx.fill();
}
}
if ( me.stroke ) {
if ( me.lineWidth ) { ctx.lineWidth = me.lineWidth; }
if ( isFunction(me.stroke) ) { me.stroke(ctx,parent); }
else {
ctx.strokeStyle = me.stroke;
ctx.stroke();
}
}
}
/**
* Callback to draw the element on the context.
* @callback draw
* @param {CanvasRenderingContext2D} ctx - 2d canvas context
* @param {array} coords - Array of the calculated `x` and `y` coordinates
* @param {canvallax.Scene|canvallax.Group} parent - Parent object, usually a `{@link canvallax.Scene}`
* @memberof! canvallax.Element
*/
});
/**
* Creates a custom Canvallax Element.
* @memberOf canvallax
* @method
*/
var createElement = canvallax.createElement = createClass.bind(null,canvallax.Element);
var kappa = 0.5522848,
// Detect support for native canvas ellipses
ellipseSupport = (function(){
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
return ( typeof ctx.ellipse === typeof function(){} );
}()),
drawEllipse = function(ctx,coords) {
ctx.ellipse(coords[0] + (this.width/2), coords[1] + (this.height / 2), (this.width/2), (this.height / 2), 0, 0, twoPI);
};
// Manually draw ellipse if browser doesn't support native canvas ellipses
if ( !ellipseSupport ) {
drawEllipse = function(ctx,coords) {
var w = this.width,
h = this.height,
x = coords[0],
y = coords[1];
if ( w === h ) { // Circle!
ctx.arc(x + w/2, coords[1] + w/2, w/2, 0, twoPI);
} else {
var ox = (w / 2) * kappa,
oy = (h / 2) * kappa,
xe = x + w,
ye = y + h,
xm = x + w / 2,
ym = y + h / 2;
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
}
};
}
/**
* Draw Ellipses and Circles!
*
* @type {canvallax.Element}
* @constructs canvallax.Ellipse
* @memberOf canvallax
* @mixes core
* @extends canvallax.Element
*
* @param {object} options - Object containing properties to be applied to the new instance. Reference the properties below.
*
* @property {number} width=null - Width of the ellipse, required to render.
* @property {number} height=null - Height of the ellipse, required to render.
*/
canvallax.Ellipse = createElement(
/** @lends canvallax.Ellipse# */
{
type: 'ellipse',
draw: drawEllipse
});
/**
* Draw a Rectangle!
*
* @class
* @mixes core
* @extends canvallax.Element
* @memberOf canvallax
*
* @param {object} options - Object containing properties to be applied to the new instance. Reference the properties below.
*
* @property {number} width=null - Width of the rectangle
* @property {number} height=null - Height of the rectangle.
*
* @example
* var redTriangle = canvallax.Polygon({
* fill: '#F00',
* points: 3,
* width: 100,
* height: 100
* });
*/
canvallax.Rectangle = createElement(
/** @lends canvallax.Rectangle.prototype */
{
type: 'rectangle',
draw: function(ctx,coords) {
ctx.rect(coords[0], coords[1], this.width, this.height);
}
});
/**
* Image class for drawing an `<img>` or `<canvas>` Element on a Canvallax scene.
*
* The element's `width` and `height` are set on image load unless already provided.
*
* @class canvallax.Image
* @mixes core
* @extends canvallax.Element
* @memberOf canvallax
*
* @param {object|string} options - Object containing properties to be applied to the new `canvallax.Image` instance, or a string containing the URL of the image src to be used.
*
* @property {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image=null - Image element to draw. Will be created on initialization if not provided.
* @property {string} src=null - URL of the image to draw, subject to [cross origin policies]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image}
* @property {number} width=null - Width of the image. Set onload if not provided.
* @property {number} height=null - Height of the image. Set onload if not provided.
*
* @example
* var img = canvallax.Image('myimage.jpg'); // src can be provided as only parameter on init.
* @example
* var img = canvallax.Image({
* src: 'myimage.jpg'
* width: 300, // Ignore the images actual dimensions, and render at a specific width
* height: 100 // Ignore the images actual dimensions, and render at a specific height
* });
*/
canvallax.Image = createElement(
/** @lends canvallax.Image# */
{
type: 'image',
onload: function(img){
img.width = ( img.width ? img.width : img.image.width );
img.height = ( img.height ? img.height : img.image.height );
},
onerror: function(){
this.removeAttribute('src');
},
init: function(options){
var img = this.image;
img = ( img && img.nodeType === 1 ? img : options && options.nodeType === 1 ? options : new Image() );
// Clone the element unless it's Canvas
if ( !(img instanceof HTMLCanvasElement) ) { img = img.cloneNode(); }
this.image = img;
// Ensure we get width/height of image for best draw performance
this.onload(this);
img.onload = this.onload.bind(null,this);
img.onerror = this.onerror;
img.src = img.src || options.src || options;
},
draw: function(ctx,coords){
if ( this.image ) {
ctx.drawImage(this.image, coords[0], coords[1], this.width, this.height);
}
}
});
/**
* Draw standard or custom polygon shapes!
*
* @class
* @mixes core
* @extends canvallax.Element
* @memberOf canvallax
*
* @param {object} options - Object containing properties to be applied to the new instance. Reference the properties below.
*
* @property {number|array} points=6 - Number of points for standard polygons (triangle, hexagon, etc) or an array of coordinates to draw more complex shapes. Set the last value of array to `'close'` if you want the shape to be closed.
* @property {number} width=null - Width of the polygon, not used if an array of points is provided.
* @property {number} height=null - Height of the polygon, not used if an array of points is provided.
*
* @example
* var redTriangle = canvallax.Polygon({
* fill: '#F00',
* points: 3,
* width: 100,
* height: 100
* });
*
* @example
* var blackStar = canvallax.Polygon({
* fill: '#000',
* points: [[80, 0], [100, 50], [160, 55], [115, 95], [130, 150], [80, 120], [30, 150], [45, 95], [0, 55], [60, 50],'close'],
* });
*
*/
canvallax.Polygon = createElement(
/** @lends canvallax.Polygon# */
{
type: 'polygon',
points: 6,
draw: function(ctx,coords) {
var p = this.points,
len = p.length,
i = 0,
w, h, x, y;
if ( len ) {
for (; i < len; i++) {
if ( p[i] === 'close' ) { ctx.closePath(); }
else { ctx[( i === 0 ? 'moveTo' : 'lineTo' )](coords[0] + p[i][0], coords[1] + p[i][1]); }
}
} else {
w = this.width / 2;
h = this.height / 2;
x = coords[0] + w;
y = coords[1] + h;
ctx.moveTo(x + w, y);
// Polygon math adapted from http://scienceprimer.com/drawing-regular-polygons-javascript-canvas
for (; i < p; i++) {
ctx.lineTo(
x+(w * Math.cos((i * twoPI) / p)),
y+(h * Math.sin((i * twoPI) / p))
);
}
ctx.closePath();
}
}
});
/**
* Tracker Class for linking an object's properties to external input, such as the scroll or pointer.
*
* @mixin
* @memberOf canvallax
*
* @param {object} options - Object containing properties to be applied to the new instance. Reference the properties below.
*
* @property {number} ease=0 - The easing of the tracked values when updated. 0 = none, higher is longer.
* @property {number} scale=1 - Multiplier of the tracked values, how drastically the tracker will affect the values. 2 is twice as fast as the tracked values, 0.5 is half the speed of the tracked values
* @property {object|number|null} offset=null - Offset that should be applied to the tracked values.
* @property {boolean|string} invert=null - Invert the tracked values. Can be a string of 'invertx' or 'inverty' to invert specific values
*
*/
canvallax.Tracker = createClass(arrayLike,animateCore,
/** @lends canvallax.Tracker# */
{
ease: 0,
scale: 1,
property: 'offset',
individual: false,
playing: true,
applyTracking: function(el, renderedPos){
var me = this,
pos = el[me.property] || {},
_pos = {};
renderedPos = renderedPos || me._render(el);
if ( !renderedPos ) { return false; }
for ( var key in renderedPos ) {
_pos[key] = me.scale * ( me.invert === true || me.invert === 'invert'+key ? renderedPos[key] : -renderedPos[key] ) +
( me.offset && !isNaN(me.offset[key]) ? me.offset[key] : !isNaN(me.offset) ? me.offset : 0 );
if ( !pos[key] ) { pos[key] = 0; }
pos[key] = ( me.ease <= 0 ? _pos[key] : pos[key] + ( _pos[key] - pos[key] ) / (me.ease + 1) );
}
el[me.property] = pos;
return this;
},
render: function() {
var len = this.length,
i = 0,
renderedPos = !this.individual ? this._render() : null;
for ( ; i < len; i++ ){ this.applyTracking(this[i],renderedPos); }
return this;
}
/**
* Create a clone of this object
* @borrows clone as clone
* @method
* @param {object=} options - Properties to be applied to the cloned object
*/
});
/**
* Creates a custom Canvallax Tracker.
* @memberOf canvallax
* @method
*/
var createTracker = canvallax.createTracker = createClass.bind(null,canvallax.Tracker);
var scrollX = 0,
scrollY = 0,
// Only one scroll tracker that works for every instance
watchingScroll = false,
onScroll = function(){
scrollX = win.pageXOffset;
scrollY = win.pageYOffset;
};
/**
* Tracker Class for linking an object's `x` and `y` to the scroll position.
*
* @class
* @mixes canvallax.Tracker
* @memberOf canvallax
*
* @param {object} options - Object containing properties to be applied to the new instance. Reference the properties below.
*
* @example
* var scene = canvallax.Scene(),
* tracker = canvallax.TrackScroll();
* scene.addTo(tracker);
*/
canvallax.TrackScroll = createTracker(
/** @lends canvallax.TrackScroll.prototype */
{
init: function(){
if ( !watchingScroll ) {
watchingScroll = true;
onScroll();
doc.addEventListener('scroll', onScroll);
}
},
_render: function(){
var e = this.element;
return {
x: e ? e.scrollLeft : scrollX,
y: e ? e.scrollTop : scrollY
};
}
});
var winHeight = win.innerHeight,
winWidth = win.innerWidth,
/** Only one window resize event that works for every instance */
watchingResize = false,
onResize = function(){
winHeight = win.innerHeight;
winWidth = win.innerWidth;
},
pointerFixedX = winWidth/2,
pointerFixedY = winHeight/2,
/** Only one pointer tracker that works for every instance */
watchingPointer = false,
onPointerMove = function(e){
pointerFixedX = ( e.touches ? e.touches[0].clientX : e.clientX );
pointerFixedY = ( e.touches ? e.touches[0].clientY : e.clientY );
};
/**
* Tracker Class for linking an object's `x` and `y` to the pointer position.
* Recommended for objects to be `fixed: true` to prevent any parent positioning from affecting the pointer tracking.
*
* @class
* @mixes canvallax.Tracker
* @memberOf canvallax
*
* @param {object} options - Object containing properties to be applied to the new instance. Reference the properties below.
*
* @example
* var scene = canvallax.Scene(),
* arrow = canvallax.Polygon({
* fill: '#000',
* points: 3,
* width: 100,
* height: 100,
* fixed: true
* }),
* tracker = canvallax.TrackPointer({ ease: 4 });
*
* arrow.addTo(scene,tracker);
*/
canvallax.TrackPointer = canvallax.createTracker(
/** @lends canvallax.TrackPointer.prototype */
{
range: false,
init: function(){
if ( !watchingPointer ) {
watchingPointer = true;
doc.addEventListener('mousemove', onPointerMove);
doc.addEventListener('touchmove', onPointerMove);
doc.addEventListener('touchstart', onPointerMove);
}
if ( !watchingResize ) {
onResize();
win.addEventListener('resize', onResize);
}
},
_render: function(){
var x = -pointerFixedX,
y = -pointerFixedY,
range = this.range;
if ( range || range === 0 ) {
x = ((pointerFixedX / winWidth) - 0.5 ) * -(range.x || range);
y = ((pointerFixedY / winHeight) - 0.5 ) * -(range.y || range);
}
return { x: x, y: y };
}
});
/**
* Element Tracker Class for linking an object's `x` and `y` to the element position, relative to scroll.
* Recommended for objects to be `fixed: true` to prevent any parent positioning from affecting the link to the element.
*
*
* @class
* @mixes canvallax.Tracker
* @memberOf canvallax
*
* @param {object} options - Object containing properties to be applied to the new instance. Reference the properties below.
*
* @property {node} element=null - Element to be tracked
*
* @example
* var scene = canvallax.Scene(),
* rect = canvallax.Rectange({
* fill: '#000',
* width: 100,
* height: 100
* }),
* tracker = canvallax.TrackElement('#myElement');
*
* rect.addTo(scene,tracker);
*/
canvallax.TrackElement = canvallax.createTracker(
/** @lends canvallax.TrackPointer.prototype */
{
init: function(opts){
var elem = (opts && opts.element) || opts;
this.element = ( typeof elem === 'string' ? document.querySelector(elem) : elem );
},
_render: function(){
var rect = this.element.getBoundingClientRect();
return { x: -rect.left, y: -rect.top };
}
});
/*
* Run canvallax plugins after canvallax and all its methods have been set up.
*/
var pluginQueue = noop,
plugins = win._clx || [],
len = plugins.length,
i = 0;
for (; i < len; i++) { plugins[i](canvallax); }
pluginQueue.push = function(fn){ fn(canvallax); };
win._clx = pluginQueue;
})(window || this);