/* Copyright (C) 2013 Justin Windle, http://soulwire.co.uk */ (function ( root, factory ) { if ( typeof exports === 'object' ) { // CommonJS like module.exports = factory(root, root.document); } else if ( typeof define === 'function' && define.amd ) { // AMD define( function() { return factory( root, root.document ); }); } else { // Browser global root.Sketch = factory( root, root.document ); } }( typeof window !== "undefined" ? window : this, function ( window, document ) { "use strict"; /* ---------------------------------------------------------------------- Config ---------------------------------------------------------------------- */ var MATH_PROPS = 'E LN10 LN2 LOG2E LOG10E PI SQRT1_2 SQRT2 abs acos asin atan ceil cos exp floor log round sin sqrt tan atan2 pow max min'.split( ' ' ); var HAS_SKETCH = '__hasSketch'; var M = Math; var CANVAS = 'canvas'; var WEBGL = 'webgl'; var DOM = 'dom'; var doc = document; var win = window; var instances = []; var defaults = { fullscreen: true, autostart: true, autoclear: true, autopause: true, container: doc.body, interval: 1, globals: true, retina: false, type: CANVAS }; var keyMap = { 8: 'BACKSPACE', 9: 'TAB', 13: 'ENTER', 16: 'SHIFT', 27: 'ESCAPE', 32: 'SPACE', 37: 'LEFT', 38: 'UP', 39: 'RIGHT', 40: 'DOWN' }; /* ---------------------------------------------------------------------- Utilities ---------------------------------------------------------------------- */ function isArray( object ) { return Object.prototype.toString.call( object ) == '[object Array]'; } function isFunction( object ) { return typeof object == 'function'; } function isNumber( object ) { return typeof object == 'number'; } function isString( object ) { return typeof object == 'string'; } function keyName( code ) { return keyMap[ code ] || String.fromCharCode( code ); } function extend( target, source, overwrite ) { for ( var key in source ) if ( overwrite || !( key in target ) ) target[ key ] = source[ key ]; return target; } function proxy( method, context ) { return function() { method.apply( context, arguments ); }; } function clone( target ) { var object = {}; for ( var key in target ) { if ( key === 'webkitMovementX' || key === 'webkitMovementY' ) continue; if ( isFunction( target[ key ] ) ) object[ key ] = proxy( target[ key ], target ); else object[ key ] = target[ key ]; } return object; } /* ---------------------------------------------------------------------- Constructor ---------------------------------------------------------------------- */ function constructor( context ) { var request, handler, target, parent, bounds, index, suffix, clock, node, copy, type, key, val, min, max, w, h; var counter = 0; var touches = []; var resized = false; var setup = false; var ratio = win.devicePixelRatio || 1; var isDiv = context.type == DOM; var is2D = context.type == CANVAS; var mouse = { x: 0.0, y: 0.0, ox: 0.0, oy: 0.0, dx: 0.0, dy: 0.0 }; var eventMap = [ context.eventTarget || context.element, pointer, 'mousedown', 'touchstart', pointer, 'mousemove', 'touchmove', pointer, 'mouseup', 'touchend', pointer, 'click', pointer, 'mouseout', pointer, 'mouseover', doc, keypress, 'keydown', 'keyup', win, active, 'focus', 'blur', resize, 'resize' ]; var keys = {}; for ( key in keyMap ) keys[ keyMap[ key ] ] = false; function trigger( method ) { if ( isFunction( method ) ) method.apply( context, [].splice.call( arguments, 1 ) ); } function bind( on ) { for ( index = 0; index < eventMap.length; index++ ) { node = eventMap[ index ]; if ( isString( node ) ) target[ ( on ? 'add' : 'remove' ) + 'EventListener' ].call( target, node, handler, false ); else if ( isFunction( node ) ) handler = node; else target = node; } } function update() { cAF( request ); request = rAF( update ); if ( !setup ) { trigger( context.setup ); setup = isFunction( context.setup ); } if ( !resized ) { trigger( context.resize ); resized = isFunction( context.resize ); } if ( context.running && !counter ) { context.dt = ( clock = +new Date() ) - context.now; context.millis += context.dt; context.now = clock; trigger( context.update ); // Pre draw if ( is2D ) { if ( context.retina ) { context.save(); if (context.autoclear) { context.scale( ratio, ratio ); } } if ( context.autoclear ) context.clear(); } // Draw trigger( context.draw ); // Post draw if ( is2D && context.retina ) context.restore(); } counter = ++counter % context.interval; } function resize() { target = isDiv ? context.style : context.canvas; suffix = isDiv ? 'px' : ''; w = context.width; h = context.height; if ( context.fullscreen ) { h = context.height = win.innerHeight; w = context.width = win.innerWidth; } if ( context.retina && is2D && ratio ) { target.style.height = h + 'px'; target.style.width = w + 'px'; w *= ratio; h *= ratio; } if ( target.height !== h ) target.height = h + suffix; if ( target.width !== w ) target.width = w + suffix; if ( is2D && !context.autoclear && context.retina ) context.scale( ratio, ratio ); if ( setup ) trigger( context.resize ); } function align( touch, target ) { bounds = target.getBoundingClientRect(); touch.x = touch.pageX - bounds.left - (win.scrollX || win.pageXOffset); touch.y = touch.pageY - bounds.top - (win.scrollY || win.pageYOffset); return touch; } function augment( touch, target ) { align( touch, context.element ); target = target || {}; target.ox = target.x || touch.x; target.oy = target.y || touch.y; target.x = touch.x; target.y = touch.y; target.dx = target.x - target.ox; target.dy = target.y - target.oy; return target; } function process( event ) { event.preventDefault(); copy = clone( event ); copy.originalEvent = event; if ( copy.touches ) { touches.length = copy.touches.length; for ( index = 0; index < copy.touches.length; index++ ) touches[ index ] = augment( copy.touches[ index ], touches[ index ] ); } else { touches.length = 0; touches[0] = augment( copy, mouse ); } extend( mouse, touches[0], true ); return copy; } function pointer( event ) { event = process( event ); min = ( max = eventMap.indexOf( type = event.type ) ) - 1; context.dragging = /down|start/.test( type ) ? true : /up|end/.test( type ) ? false : context.dragging; while( min ) isString( eventMap[ min ] ) ? trigger( context[ eventMap[ min-- ] ], event ) : isString( eventMap[ max ] ) ? trigger( context[ eventMap[ max++ ] ], event ) : min = 0; } function keypress( event ) { key = event.keyCode; val = event.type == 'keyup'; keys[ key ] = keys[ keyName( key ) ] = !val; trigger( context[ event.type ], event ); } function active( event ) { if ( context.autopause ) ( event.type == 'blur' ? stop : start )(); trigger( context[ event.type ], event ); } // Public API function start() { context.now = +new Date(); context.running = true; } function stop() { context.running = false; } function toggle() { ( context.running ? stop : start )(); } function clear() { if ( is2D ) context.clearRect( 0, 0, context.width * ratio, context.height * ratio ); } function destroy() { parent = context.element.parentNode; index = instances.indexOf( context ); if ( parent ) parent.removeChild( context.element ); if ( ~index ) instances.splice( index, 1 ); bind( false ); stop(); } extend( context, { touches: touches, mouse: mouse, keys: keys, dragging: false, running: false, millis: 0, now: NaN, dt: NaN, destroy: destroy, toggle: toggle, clear: clear, start: start, stop: stop }); instances.push( context ); return ( context.autostart && start(), bind( true ), resize(), update(), context ); } /* ---------------------------------------------------------------------- Global API ---------------------------------------------------------------------- */ var element, context, Sketch = { CANVAS: CANVAS, WEB_GL: WEBGL, WEBGL: WEBGL, DOM: DOM, instances: instances, install: function( context ) { if ( !context[ HAS_SKETCH ] ) { for ( var i = 0; i < MATH_PROPS.length; i++ ) context[ MATH_PROPS[i] ] = M[ MATH_PROPS[i] ]; extend( context, { TWO_PI: M.PI * 2, HALF_PI: M.PI / 2, QUARTER_PI: M.PI / 4, random: function( min, max ) { if ( isArray( min ) ) return min[ ~~( M.random() * min.length ) ]; if ( !isNumber( max ) ) max = min || 1, min = 0; return min + M.random() * ( max - min ); }, lerp: function( min, max, amount ) { return min + amount * ( max - min ); }, map: function( num, minA, maxA, minB, maxB ) { return ( num - minA ) / ( maxA - minA ) * ( maxB - minB ) + minB; } }); context[ HAS_SKETCH ] = true; } }, create: function( options ) { options = extend( options || {}, defaults ); if ( options.globals ) Sketch.install( self ); element = options.element = options.element || doc.createElement( options.type === DOM ? 'div' : 'canvas' ); context = options.context = options.context || (function() { switch( options.type ) { case CANVAS: return element.getContext( '2d', options ); case WEBGL: return element.getContext( 'webgl', options ) || element.getContext( 'experimental-webgl', options ); case DOM: return element.canvas = element; } })(); ( options.container || doc.body ).appendChild( element ); return Sketch.augment( context, options ); }, augment: function( context, options ) { options = extend( options || {}, defaults ); options.element = context.canvas || context; options.element.className += ' sketch'; extend( context, options, true ); return constructor( context ); } }; /* ---------------------------------------------------------------------- Shims ---------------------------------------------------------------------- */ var vendors = [ 'ms', 'moz', 'webkit', 'o' ]; var scope = self; var then = 0; var a = 'AnimationFrame'; var b = 'request' + a; var c = 'cancel' + a; var rAF = scope[ b ]; var cAF = scope[ c ]; for ( var i = 0; i < vendors.length && !rAF; i++ ) { rAF = scope[ vendors[ i ] + 'Request' + a ]; cAF = scope[ vendors[ i ] + 'Cancel' + a ]; } scope[ b ] = rAF = rAF || function( callback ) { var now = +new Date(); var dt = M.max( 0, 16 - ( now - then ) ); var id = setTimeout( function() { callback( now + dt ); }, dt ); then = now + dt; return id; }; scope[ c ] = cAF = cAF || function( id ) { clearTimeout( id ); }; /* ---------------------------------------------------------------------- Output ---------------------------------------------------------------------- */ return Sketch; }));