/*! 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);