(function () {
/**
 * almond 0.2.5 Copyright (c) 2011-2012, The Dojo Foundation All Rights Reserved.
 * Available via the MIT or new BSD license.
 * see: http://github.com/jrburke/almond for details
 */
//Going sloppy to avoid 'use strict' string cost, but strict practices should
//be followed.
/*jslint sloppy: true */
/*global setTimeout: false */

var requirejs, require, define;
(function (undef) {
    var main, req, makeMap, handlers,
        defined = {},
        waiting = {},
        config = {},
        defining = {},
        hasOwn = Object.prototype.hasOwnProperty,
        aps = [].slice;

    function hasProp(obj, prop) {
        return hasOwn.call(obj, prop);
    }

    /**
     * Given a relative module name, like ./something, normalize it to
     * a real name that can be mapped to a path.
     * @param {String} name the relative name
     * @param {String} baseName a real name that the name arg is relative
     * to.
     * @returns {String} normalized name
     */
    function normalize(name, baseName) {
        var nameParts, nameSegment, mapValue, foundMap,
            foundI, foundStarMap, starI, i, j, part,
            baseParts = baseName && baseName.split("/"),
            map = config.map,
            starMap = (map && map['*']) || {};

        //Adjust any relative paths.
        if (name && name.charAt(0) === ".") {
            //If have a base name, try to normalize against it,
            //otherwise, assume it is a top-level require that will
            //be relative to baseUrl in the end.
            if (baseName) {
                //Convert baseName to array, and lop off the last part,
                //so that . matches that "directory" and not name of the baseName's
                //module. For instance, baseName of "one/two/three", maps to
                //"one/two/three.js", but we want the directory, "one/two" for
                //this normalization.
                baseParts = baseParts.slice(0, baseParts.length - 1);

                name = baseParts.concat(name.split("/"));

                //start trimDots
                for (i = 0; i < name.length; i += 1) {
                    part = name[i];
                    if (part === ".") {
                        name.splice(i, 1);
                        i -= 1;
                    } else if (part === "..") {
                        if (i === 1 && (name[2] === '..' || name[0] === '..')) {
                            //End of the line. Keep at least one non-dot
                            //path segment at the front so it can be mapped
                            //correctly to disk. Otherwise, there is likely
                            //no path mapping for a path starting with '..'.
                            //This can still fail, but catches the most reasonable
                            //uses of ..
                            break;
                        } else if (i > 0) {
                            name.splice(i - 1, 2);
                            i -= 2;
                        }
                    }
                }
                //end trimDots

                name = name.join("/");
            } else if (name.indexOf('./') === 0) {
                // No baseName, so this is ID is resolved relative
                // to baseUrl, pull off the leading dot.
                name = name.substring(2);
            }
        }

        //Apply map config if available.
        if ((baseParts || starMap) && map) {
            nameParts = name.split('/');

            for (i = nameParts.length; i > 0; i -= 1) {
                nameSegment = nameParts.slice(0, i).join("/");

                if (baseParts) {
                    //Find the longest baseName segment match in the config.
                    //So, do joins on the biggest to smallest lengths of baseParts.
                    for (j = baseParts.length; j > 0; j -= 1) {
                        mapValue = map[baseParts.slice(0, j).join('/')];

                        //baseName segment has  config, find if it has one for
                        //this name.
                        if (mapValue) {
                            mapValue = mapValue[nameSegment];
                            if (mapValue) {
                                //Match, update name to the new value.
                                foundMap = mapValue;
                                foundI = i;
                                break;
                            }
                        }
                    }
                }

                if (foundMap) {
                    break;
                }

                //Check for a star map match, but just hold on to it,
                //if there is a shorter segment match later in a matching
                //config, then favor over this star map.
                if (!foundStarMap && starMap && starMap[nameSegment]) {
                    foundStarMap = starMap[nameSegment];
                    starI = i;
                }
            }

            if (!foundMap && foundStarMap) {
                foundMap = foundStarMap;
                foundI = starI;
            }

            if (foundMap) {
                nameParts.splice(0, foundI, foundMap);
                name = nameParts.join('/');
            }
        }

        return name;
    }

    function makeRequire(relName, forceSync) {
        return function () {
            //A version of a require function that passes a moduleName
            //value for items that may need to
            //look up paths relative to the moduleName
            return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync]));
        };
    }

    function makeNormalize(relName) {
        return function (name) {
            return normalize(name, relName);
        };
    }

    function makeLoad(depName) {
        return function (value) {
            defined[depName] = value;
        };
    }

    function callDep(name) {
        if (hasProp(waiting, name)) {
            var args = waiting[name];
            delete waiting[name];
            defining[name] = true;
            main.apply(undef, args);
        }

        if (!hasProp(defined, name) && !hasProp(defining, name)) {
            throw new Error('No ' + name);
        }
        return defined[name];
    }

    //Turns a plugin!resource to [plugin, resource]
    //with the plugin being undefined if the name
    //did not have a plugin prefix.
    function splitPrefix(name) {
        var prefix,
            index = name ? name.indexOf('!') : -1;
        if (index > -1) {
            prefix = name.substring(0, index);
            name = name.substring(index + 1, name.length);
        }
        return [prefix, name];
    }

    /**
     * Makes a name map, normalizing the name, and using a plugin
     * for normalization if necessary. Grabs a ref to plugin
     * too, as an optimization.
     */
    makeMap = function (name, relName) {
        var plugin,
            parts = splitPrefix(name),
            prefix = parts[0];

        name = parts[1];

        if (prefix) {
            prefix = normalize(prefix, relName);
            plugin = callDep(prefix);
        }

        //Normalize according
        if (prefix) {
            if (plugin && plugin.normalize) {
                name = plugin.normalize(name, makeNormalize(relName));
            } else {
                name = normalize(name, relName);
            }
        } else {
            name = normalize(name, relName);
            parts = splitPrefix(name);
            prefix = parts[0];
            name = parts[1];
            if (prefix) {
                plugin = callDep(prefix);
            }
        }

        //Using ridiculous property names for space reasons
        return {
            f: prefix ? prefix + '!' + name : name, //fullName
            n: name,
            pr: prefix,
            p: plugin
        };
    };

    function makeConfig(name) {
        return function () {
            return (config && config.config && config.config[name]) || {};
        };
    }

    handlers = {
        require: function (name) {
            return makeRequire(name);
        },
        exports: function (name) {
            var e = defined[name];
            if (typeof e !== 'undefined') {
                return e;
            } else {
                return (defined[name] = {});
            }
        },
        module: function (name) {
            return {
                id: name,
                uri: '',
                exports: defined[name],
                config: makeConfig(name)
            };
        }
    };

    main = function (name, deps, callback, relName) {
        var cjsModule, depName, ret, map, i,
            args = [],
            usingExports;

        //Use name if no relName
        relName = relName || name;

        //Call the callback to define the module, if necessary.
        if (typeof callback === 'function') {

            //Pull out the defined dependencies and pass the ordered
            //values to the callback.
            //Default to [require, exports, module] if no deps
            deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
            for (i = 0; i < deps.length; i += 1) {
                map = makeMap(deps[i], relName);
                depName = map.f;

                //Fast path CommonJS standard dependencies.
                if (depName === "require") {
                    args[i] = handlers.require(name);
                } else if (depName === "exports") {
                    //CommonJS module spec 1.1
                    args[i] = handlers.exports(name);
                    usingExports = true;
                } else if (depName === "module") {
                    //CommonJS module spec 1.1
                    cjsModule = args[i] = handlers.module(name);
                } else if (hasProp(defined, depName) ||
                           hasProp(waiting, depName) ||
                           hasProp(defining, depName)) {
                    args[i] = callDep(depName);
                } else if (map.p) {
                    map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
                    args[i] = defined[depName];
                } else {
                    throw new Error(name + ' missing ' + depName);
                }
            }

            ret = callback.apply(defined[name], args);

            if (name) {
                //If setting exports via "module" is in play,
                //favor that over return value and exports. After that,
                //favor a non-undefined return value over exports use.
                if (cjsModule && cjsModule.exports !== undef &&
                        cjsModule.exports !== defined[name]) {
                    defined[name] = cjsModule.exports;
                } else if (ret !== undef || !usingExports) {
                    //Use the return value from the function.
                    defined[name] = ret;
                }
            }
        } else if (name) {
            //May just be an object definition for the module. Only
            //worry about defining if have a module name.
            defined[name] = callback;
        }
    };

    requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
        if (typeof deps === "string") {
            if (handlers[deps]) {
                //callback in this case is really relName
                return handlers[deps](callback);
            }
            //Just return the module wanted. In this scenario, the
            //deps arg is the module name, and second arg (if passed)
            //is just the relName.
            //Normalize module name, if it contains . or ..
            return callDep(makeMap(deps, callback).f);
        } else if (!deps.splice) {
            //deps is a config object, not an array.
            config = deps;
            if (callback.splice) {
                //callback is an array, which means it is a dependency list.
                //Adjust args if there are dependencies
                deps = callback;
                callback = relName;
                relName = null;
            } else {
                deps = undef;
            }
        }

        //Support require(['a'])
        callback = callback || function () {};

        //If relName is a function, it is an errback handler,
        //so remove it.
        if (typeof relName === 'function') {
            relName = forceSync;
            forceSync = alt;
        }

        //Simulate async callback;
        if (forceSync) {
            main(undef, deps, callback, relName);
        } else {
            //Using a non-zero value because of concern for what old browsers
            //do, and latest browsers "upgrade" to 4 if lower value is used:
            //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
            //If want a value immediately, use require('id') instead -- something
            //that works in almond on the global level, but not guaranteed and
            //unlikely to work in other AMD implementations.
            setTimeout(function () {
                main(undef, deps, callback, relName);
            }, 4);
        }

        return req;
    };

    /**
     * Just drops the config on the floor, but returns req in case
     * the config return value is used.
     */
    req.config = function (cfg) {
        config = cfg;
        if (config.deps) {
            req(config.deps, config.callback);
        }
        return req;
    };

    define = function (name, deps, callback) {

        //This module may not have dependencies
        if (!deps.splice) {
            //deps is not an array, so probably means
            //an object literal or factory function for
            //the value. Adjust args.
            callback = deps;
            deps = [];
        }

        if (!hasProp(defined, name) && !hasProp(waiting, name)) {
            waiting[name] = [name, deps, callback];
        }
    };

    define.amd = {
        jQuery: true
    };
}());

define("almond", function(){});

// Copyright 2002-2013, University of Colorado Boulder
/**
 * Launches a PhET Simulation, after preloading the specified images.
 *
 * @author Sam Reid
 */
define('JOIST/SimLauncher',[], function() {
  

  return {
    /**
     * Launch the Sim by preloading the images and calling the callback.
     *
     * TODO: add an awesome loading screen
     *
     * @param callback the callback function which should create and start the sim, given that the images are loaded
     */
    launch: function( callback ) {

      // image elements to remove once we are fully loaded
      var elementsToRemove = [];

      function doneLoadingImages() {
        $( '#splash' ).remove();
        callback();
      }

      // if image dimensions exist, immediately fire the "all images loaded" event
      var loaded = 0;

      //For the images that were written to base64 format using requirejs, make sure they are loaded.
      //img.src = base64 is asynchronous on IE10 and OSX/Safari, so we have to make sure they loaded before returning.
      if ( window.phetImages ) {
        for ( var i = 0; i < window.phetImages.length; i++ ) {
          var phetImage = window.phetImages[i];
          phetImage.onload = function() {
            loaded++;
            if ( loaded === window.phetImages.length ) {
              doneLoadingImages();
            }
          };
        }
      }
      else {
        doneLoadingImages();
      }

      $( window ).load( function() {
        // if images were not loaded immediately, signal the "all images loaded" event

        // we wait for here to remove the images from the DOM, otherwise IE9/10 treat the images as completely blank!
        _.each( elementsToRemove, function( element ) {

          //TODO: Why is this null sometimes?
          if ( element.parentNode ) {
            element.parentNode.removeChild( element );
          }
        } );
      } );
    }};
} );
// Copyright 2002-2013, University of Colorado Boulder

/*
 * Usage:
 * var assert = require( '<assert>' )( 'flagName' );
 *
 * assert && assert( <simple value or big computation>, "<message here>" );
 *
 * TODO: decide on usages and viability, and if so document further
 *
 * NOTE: for changing build, add has.js tests for 'assert.' + flagName
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('ASSERT/assert',['require'], function( require ) {
  
  
  // CAUTION: if using the AST modifier for assertions, do not separate this out into its own 'assert' variable
  return function assert( name, excludeByDefault ) {
    var hasName = 'assert.' + name;
    
    var flagDefined = window.has && window.has( hasName ) !== undefined;
    var skipAssert = flagDefined ? !window.has( hasName ) : excludeByDefault;
    
    if ( skipAssert ) {
      return null;
    } else {
      return function( predicate, message ) {
        var result = typeof predicate === 'function' ? predicate() : predicate;
        
        if ( !result ) {

          //Log the stack trace to IE.  Just creating an Error is not enough, it has to be caught to get a stack.
          //TODO: What will this do for IE9?  Probably just print stack = undefined.
          if ( window.navigator && window.navigator.appName === 'Microsoft Internet Explorer' ) {
            try { throw new Error(); }
            catch( e ) { message = message + ", stack=\n" + e.stack; }
          }
          
          // TODO: custom error?
          throw new Error( 'Assertion failed: ' + message );
        }
      };
    }
  };
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Object instance allocation tracking, so we can cut down on garbage collection.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('PHET_CORE/phetAllocation',['require'], function( require ) {
  
  
  return function phetAllocation( name ) {
    if ( window.alloc ) {
      var stack;
      try { throw new Error(); } catch ( e ) { stack = e.stack; }
      
      if ( !window.alloc[name] ) {
        window.alloc[name] = { count: 0, stacks: {} };
      }
      var log = window.alloc[name];
      
      log.count++;
      if ( !log.stacks[stack] ) {
        log.stacks[stack] = 1;
      } else {
        log.stacks[stack] += 1;
      }
      log.report = function() {
        var stacks = Object.keys( log.stacks );
        stacks = _.sortBy( stacks, function( key ) { return log.stacks[key]; } );
        _.each( stacks, function( stack ) {
          console.log( log.stacks[stack] + ': ' + stack );
        } );
      };
    }
  };
} );

// Copyright 2002-2013, University of Colorado

/**
 * The main 'scenery' namespace object for the exported (non-Require.js) API. Used internally
 * since it prevents Require.js issues with circular dependencies.
 *
 * The returned scenery object namespace may be incomplete if not all modules are listed as
 * dependencies. Please use the 'main' module for that purpose if all of Scenery is desired.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/scenery',['require','ASSERT/assert','ASSERT/assert','PHET_CORE/phetAllocation'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'scenery' );
  
  window.sceneryAssert = assert;
  window.sceneryAssertExtra = require( 'ASSERT/assert' )( 'scenery.extra' );
  
  window.sceneryLayerLog = null;
  window.sceneryEventLog = null;
  window.sceneryAccessibilityLog = null;
  
  // object allocation tracking
  window.phetAllocation = require( 'PHET_CORE/phetAllocation' );
  
  var scratchCanvas = document.createElement( 'canvas' );
  var scratchContext = scratchCanvas.getContext( '2d' );
  
  // will be filled in by other modules
  return {
    assert: assert,
    
    scratchCanvas: scratchCanvas,   // a canvas used for convenience functions (think of it as having arbitrary state)
    scratchContext: scratchContext, // a context used for convenience functions (think of it as having arbitrary state)
    
    enableLayerLogging: function() {
      window.sceneryLayerLog = function( ob ) { console.log( ob ); };
    },
  
    disableLayerLogging: function() {
      window.sceneryLayerLog = null;
    },
    
    enableEventLogging: function() {
      window.sceneryEventLog = function( ob ) { console.log( ob ); };
    },
  
    disableEventLogging: function() {
      window.sceneryEventLog = null;
    },
    
    enableAccessibilityLogging: function() {
      window.sceneryAccessibilityLog = function( ob ) { console.log( ob ); };
    },
  
    disableAccessibilityLogging: function() {
      window.sceneryAccessibilityLog = null;
    }
  };
} );


// Copyright 2002-2013, University of Colorado Boulder

define('DOT/dot',['require','ASSERT/assert','PHET_CORE/phetAllocation'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'dot' );
  
  // object allocation tracking
  window.phetAllocation = require( 'PHET_CORE/phetAllocation' );
  
  var dot = function dot() {
    switch ( arguments.length ) {
      case 2:
        return new dot.Vector2( arguments[0], arguments[1] );
      case 3:
        return new dot.Vector3( arguments[0], arguments[1], arguments[2] );
      case 4:
        return new dot.Vector4( arguments[0], arguments[1], arguments[2], arguments[3] );
      default:
        throw new Error( 'dot takes 2-4 arguments' );
    }
  };
  
  // TODO: performance: check browser speed to compare how fast this is. We may need to add a 32 option for GL ES.
  dot.FastArray = window.Float64Array ? window.Float64Array : window.Array;
  
  dot.assert = assert;
  
  // will be filled in by other modules
  return dot;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Like Underscore's _.extend, but with hardcoded support for ES5 getters/setters.
 *
 * See https://github.com/documentcloud/underscore/pull/986.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('PHET_CORE/extend',['require'], function( require ) {
  
  
  return function extend( obj ) {
    _.each( Array.prototype.slice.call( arguments, 1 ), function( source ) {
      if ( source ) {
        for ( var prop in source ) {
          Object.defineProperty( obj, prop, Object.getOwnPropertyDescriptor( source, prop ) );
        }
      }
    });
    return obj;
  };
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Experimental prototype inheritance
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */
define('PHET_CORE/inherit',['require','PHET_CORE/extend'], function( require ) {
  
  
  var extend = require( 'PHET_CORE/extend' );
  
  /**
   * Experimental inheritance prototype, similar to Inheritance.inheritPrototype, but maintains
   * supertype.prototype.constructor while properly copying ES5 getters and setters.
   *
   * TODO: find problems with this! It's effectively what is being used by Scenery
   * TODO: consider inspecting arguments to see whether they are functions or just objects, to support
   *       something like inherit( subtype, supertypeA, supertypeB, properties )
   *
   * Usage:
   * function A() { scenery.Node.call( this ); };
   * inherit( scenery.Node, A, {
   *   customBehavior: function() { ... },
   *   isAnA: true
   * } );
   * new A().isAnA // true
   * new scenery.Node().isAnA // undefined
   * new A().constructor.name // 'A'
   *
   * @param subtype             Constructor for the subtype. Generally should contain supertype.call( this, ... )
   * @param supertype           Constructor for the supertype.
   * @param prototypeProperties [optional] object containing properties that will be set on the prototype.
   * @param staticProperties [optional] object containing properties that will be set on the constructor function itself
   */
  function inherit( supertype, subtype, prototypeProperties, staticProperties ) {
    function F() {}
    F.prototype = supertype.prototype; // so new F().__proto__ === supertype.prototype
    
    subtype.prototype = extend( // extend will combine the properties and constructor into the new F copy
      new F(),                  // so new F().__proto__ === supertype.prototype, and the prototype chain is set up nicely
      { constructor: subtype }, // overrides the constructor properly
      prototypeProperties       // [optional] additional properties for the prototype, as an object.
    );

    //Copy the static properties onto the subtype constructor so they can be accessed 'statically'
    extend( subtype, staticProperties );
    
    return subtype; // pass back the subtype so it can be returned immediately as a module export
  }

  return inherit;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Experimental object pooling mix-in
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('PHET_CORE/Poolable',['require','PHET_CORE/extend'], function( require ) {
  
  
  var extend = require( 'PHET_CORE/extend' );
  
  /*
   * For option details, please see documentation inside this constructor body for now
   */
  return function Poolable( type, options ) {
    var proto = type.prototype;
    
    // defaults
    options = extend( {
      maxPoolSize: 50, // since we don't want to blow too much memory
      initialSize: 0
    }, options );
    
    var pool = type.pool = [];
    
    /*
     * For example: defaultFactory: function() { return new Vector2(); }
     */
    if ( options.defaultFactory ) {
      type.dirtyFromPool = function() {
        if ( pool.length ) {
          // return an instance in an arbitrary (dirty) state
          return pool.pop();
        } else {
          // else return a new default instance
          return options.defaultFactory();
        }
      };
      
      // fills the object pool up to n instances
      type.fillPool = function( n ) {
        // fill up the object pool to the initial size
        while ( pool.length < n ) {
          pool.push( options.defaultFactory() );
        }
      };
      
      // fill the pool initially to the initial size
      type.fillPool( options.initialSize );
    }
    
    /*
     * For example: constructorDuplicateFactory:
     *                function( pool ) {
     *                  return function( x, y ) {
     *                    if ( pool.length ) {
     *                      return pool.pop().set( x, y );
     *                    } else {
     *                      return new Vector2( x, y );
     *                    }
     *                  }
     *                }
     * It allows arbitrary creation (from the constructor / etc) or mutation (from the pooled instance).
     */
    if ( options.constructorDuplicateFactory ) {
      type.createFromPool = options.constructorDuplicateFactory( pool );
    }
    
    /*
     * Frees the object to the pool (instance.freeToPool())
     */
    proto.freeToPool = function() {
      if ( pool.length < options.maxPoolSize ) {
        pool.push( this );
      }
    };
  };
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Utility functions for Dot, placed into the dot.X namespace.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Util',['require','ASSERT/assert','DOT/dot'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'dot' );
  
  var dot = require( 'DOT/dot' );
  // require( 'DOT/Vector2' ); // Require.js doesn't like the circular reference
  
  dot.Util = {
    testAssert: function() {
      return 'assert.dot: ' + ( assert ? 'true' : 'false' );
    },
    
    clamp: function( value, min, max ) {
      if ( value < min ) {
        return min;
      }
      else if ( value > max ) {
        return max;
      }
      else {
        return value;
      }
    },
    
    // returns a number between [min,max) with the same equivalence class as value mod (max-min)
    moduloBetweenDown: function( value, min, max ) {
      assert && assert( max > min, 'max > min required for moduloBetween' );
      
      var divisor = max - min;
      
      // get a partial result of value-min between [0,divisor)
      var partial = ( value - min ) % divisor;
      if ( partial < 0 ) {
        // since if value-min < 0, the remainder will give us a negative number
        partial += divisor;
      }
      
      return partial + min; // add back in the minimum value
    },
    
    // returns a number between (min,max] with the same equivalence class as value mod (max-min)
    moduloBetweenUp: function( value, min, max ) {
      return -Util.moduloBetweenDown( -value, -max, -min );
    },
    
    // Returns an array of integers from A to B (including both A to B)
    rangeInclusive: function( a, b ) {
      if ( b < a ) {
        return [];
      }
      var result = new Array( b - a + 1 );
      for ( var i = a; i <= b; i++ ) {
        result[i-a] = i;
      }
      return result;
    },
    
    // Returns an array of integers between A and B (excluding both A to B)
    rangeExclusive: function( a, b ) {
      return Util.rangeInclusive( a + 1, b - 1 );
    },
    
    toRadians: function( degrees ) {
      return Math.PI * degrees / 180;
    },
    
    toDegrees: function( radians ) {
      return 180 * radians / Math.PI;
    },
    
    // intersection between the line from p1-p2 and the line from p3-p4
    lineLineIntersection: function( p1, p2, p3, p4 ) {
      var x12 = p1.x - p2.x;
      var x34 = p3.x - p4.x;
      var y12 = p1.y - p2.y;
      var y34 = p3.y - p4.y;
      
      var denom = x12 * y34 - y12 * x34;
      
      var a = p1.x * p2.y - p1.y * p2.x;
      var b = p3.x * p4.y - p3.y * p4.x;
      
      return new dot.Vector2(
        ( a * x34 - x12 * b ) / denom,
        ( a * y34 - y12 * b ) / denom
      );
    },
    
    // return an array of real roots of ax^2 + bx + c = 0
    solveQuadraticRootsReal: function( a, b, c ) {
      var epsilon = 1E7;
      
      //We need to test whether a is several orders of magnitude less than b or c. If so, return the result as a solution to the linear (easy) equation
      if ( a === 0 || Math.abs( b / a ) > epsilon || Math.abs( c / a ) > epsilon ) {
        return [ -c / b ];
      }
      
      var discriminant = b * b - 4 * a * c;
      if ( discriminant < 0 ) {
        return [];
      }
      var sqrt = Math.sqrt( discriminant );
      // TODO: how to handle if discriminant is 0? give unique root or double it?
      // TODO: probably just use Complex for the future
      return [
        ( -b - sqrt ) / ( 2 * a ),
        ( -b + sqrt ) / ( 2 * a )
      ];
    },
    
    // return an array of real roots of ax^3 + bx^2 + cx + d = 0
    solveCubicRootsReal: function( a, b, c, d ) {
      // TODO: a Complex type!
      
      //We need to test whether a is several orders of magnitude less than b, c, d
      var epsilon = 1E7;
      
      if ( a === 0 || Math.abs( b / a ) > epsilon || Math.abs( c / a ) > epsilon || Math.abs( d / a ) > epsilon ) {
        return Util.solveQuadraticRootsReal( b, c, d );
      }
      if ( d === 0 || Math.abs( a / d ) > epsilon || Math.abs( b / d ) > epsilon || Math.abs( c / d ) > epsilon ) {
        return Util.solveQuadraticRootsReal( a, b, c );
      }
      
      b /= a;
      c /= a;
      d /= a;
      
      var s, t;
      var q = ( 3.0 * c - ( b * b ) ) / 9;
      var r = ( -(27 * d) + b * (9 * c - 2 * (b * b)) ) / 54;
      var discriminant = q  * q  * q + r  * r;
      var b3 = b / 3;
      
      if ( discriminant > 0 ) {
        // a single real root
        var dsqrt = Math.sqrt( discriminant );
        return [ Util.cubeRoot( r + dsqrt ) + Util.cubeRoot( r - dsqrt ) - b3 ];
      }
      
      // three real roots
      if ( discriminant === 0 ) {
        // contains a double root
        var rsqrt = Util.cubeRoot( r );
        var doubleRoot = b3 - rsqrt;
        return [ -b3 + 2 * rsqrt, doubleRoot, doubleRoot ];
      } else {
        // all unique
        var qX = -q * q * q;
        qX = Math.acos( r / Math.sqrt( qX ) );
        var rr = 2 * Math.sqrt( -q );
        return [
          -b3 + rr * Math.cos( qX / 3 ),
          -b3 + rr * Math.cos( ( qX + 2 * Math.PI ) / 3 ),
          -b3 + rr * Math.cos( ( qX + 4 * Math.PI ) / 3 )
        ];
      }
    },
    
    cubeRoot: function( x ) {
      return x >= 0 ? Math.pow( x, 1/3 ) : -Math.pow( -x, 1/3 );
    },

    // Linearly interpolate two points and evaluate the line equation for a third point
    // f( a1 ) = b1, f( a2 ) = b2, f( a3 ) = <linear mapped value>
    linear: function( a1, a2, b1, b2, a3 ) {
      return ( b2 - b1 ) / ( a2 - a1 ) * ( a3 - a1 ) + b1;
    },

    /**
     * A predictable implementation of toFixed.
     * JavaScript's toFixed is notoriously buggy, behavior differs depending on browser,
     * because the spec doesn't specify whether to round or floor.
     */
    toFixed: function( number, decimalPlaces ) {
      var multiplier = Math.pow( 10, decimalPlaces );
      return Math.round( number * multiplier ) / multiplier;
    },

    isInteger: function( number ) {
      return Math.floor( number ) === number;
    },

    /*
     * Computes the intersection of two line segments. Algorithm taked from Paul Bourke, 1989:
     * http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/
     * Ported from MathUtil.java on 9/20/2013 by @samreid
     * line a goes from point 1->2 and line b goes from 3->4
     * @returns a Vector2 of the intersection point, or null if no intersection
     */
    lineSegmentIntersection: function( x1, y1, x2, y2, x3, y3, x4, y4 ) {
      var numA = ( x4 - x3 ) * ( y1 - y3 ) - ( y4 - y3 ) * ( x1 - x3 );
      var numB = ( x2 - x1 ) * ( y1 - y3 ) - ( y2 - y1 ) * ( x1 - x3 );
      var denom = ( y4 - y3 ) * ( x2 - x1 ) - ( x4 - x3 ) * ( y2 - y1 );

      // If denominator is 0, the lines are parallel or coincident
      if ( denom === 0 ) {
        return null;
      }
      else {
        var ua = numA / denom;
        var ub = numB / denom;

        // ua and ub must both be in the range 0 to 1 for the segments to have an intersection pt.
        if ( !( ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1 ) ) {
          return null;
        }
        else {
          var x = x1 + ua * ( x2 - x1 );
          var y = y1 + ua * ( y2 - y1 );
          return new dot.Vector2( x, y );
        }
      }
    }
  };
  var Util = dot.Util;
  
  // make these available in the main namespace directly (for now)
  dot.testAssert = Util.testAssert;
  dot.clamp = Util.clamp;
  dot.moduloBetweenDown = Util.moduloBetweenDown;
  dot.moduloBetweenUp = Util.moduloBetweenUp;
  dot.rangeInclusive = Util.rangeInclusive;
  dot.rangeExclusive = Util.rangeExclusive;
  dot.toRadians = Util.toRadians;
  dot.toDegrees = Util.toDegrees;
  dot.lineLineIntersection = Util.lineLineIntersection;
  dot.solveQuadraticRootsReal = Util.solveQuadraticRootsReal;
  dot.solveCubicRootsReal = Util.solveCubicRootsReal;
  dot.cubeRoot = Util.cubeRoot;
  dot.linear = Util.linear;
  
  return Util;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Basic 2-dimensional vector
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Vector2',['require','ASSERT/assert','DOT/dot','PHET_CORE/inherit','PHET_CORE/Poolable','DOT/Util'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'dot' );
  
  var dot = require( 'DOT/dot' );
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Poolable = require( 'PHET_CORE/Poolable' );
  require( 'DOT/Util' );
  // require( 'DOT/Vector3' ); // commented out since Require.js complains about the circular dependency
  
  dot.Vector2 = function Vector2( x, y ) {
    // allow optional parameters
    this.x = x || 0;
    this.y = y || 0;
    
    assert && assert( typeof this.x === 'number', 'x needs to be a number' );
    assert && assert( typeof this.y === 'number', 'y needs to be a number' );
    
    phetAllocation && phetAllocation( 'Vector2' );
  };
  var Vector2 = dot.Vector2;
  
  Vector2.createPolar = function( magnitude, angle ) {
    return new Vector2( magnitude * Math.cos( angle ), magnitude * Math.sin( angle ) );
  };
  
  Vector2.prototype = {
    constructor: Vector2,
    
    isVector2: true,
    
    dimension: 2,
    
    magnitude: function() {
      return Math.sqrt( this.magnitudeSquared() );
    },
    
    magnitudeSquared: function() {
      return this.x * this.x + this.y * this.y;
    },
    
    // the distance between this vector (treated as a point) and another point
    distance: function( point ) {
      return Math.sqrt( this.distanceSquared( point ) );
    },

    // the distance between this vector (treated as a point) and another point specified as x:Number, y:Number
    distanceXY: function( x, y ) {
      var dx = this.x - x;
      var dy = this.y - y;
      return Math.sqrt( dx * dx + dy * dy );
    },
    
    // the squared distance between this vector (treated as a point) and another point
    distanceSquared: function( point ) {
      var dx = this.x - point.x;
      var dy = this.y - point.y;
      return dx * dx + dy * dy;
    },
    
    dot: function( v ) {
      return this.x * v.x + this.y * v.y;
    },
    
    equals: function( other ) {
      return this.x === other.x && this.y === other.y;
    },
    
    equalsEpsilon: function( other, epsilon ) {
      if ( !epsilon ) {
        epsilon = 0;
      }
      return Math.max( Math.abs( this.x - other.x ), Math.abs( this.y - other.y ) ) <= epsilon;
    },
    
    isFinite: function() {
      return isFinite( this.x ) && isFinite( this.y );
    },
    
    /*---------------------------------------------------------------------------*
     * Immutables
     *----------------------------------------------------------------------------*/
    
    copy: function() {
      return new Vector2( this.x, this.y );
    },
    
    // z component of the equivalent 3-dimensional cross product (this.x, this.y,0) x (v.x, v.y, 0)
    crossScalar: function( v ) {
      return this.x * v.y - this.y * v.x;
    },
    
    normalized: function() {
      var mag = this.magnitude();
      if ( mag === 0 ) {
        throw new Error( "Cannot normalize a zero-magnitude vector" );
      }
      else {
        return new Vector2( this.x / mag, this.y / mag );
      }
    },
    
    timesScalar: function( scalar ) {
      return new Vector2( this.x * scalar, this.y * scalar );
    },
    
    times: function( scalar ) {
      // make sure it's not a vector!
      assert && assert( scalar.dimension === undefined );
      return this.timesScalar( scalar );
    },
    
    componentTimes: function( v ) {
      return new Vector2( this.x * v.x, this.y * v.y );
    },
    
    plus: function( v ) {
      return new Vector2( this.x + v.x, this.y + v.y );
    },

    plusXY: function( x, y ) {
      return new Vector2( this.x + x, this.y + y );
    },

    plusScalar: function( scalar ) {
      return new Vector2( this.x + scalar, this.y + scalar );
    },
    
    minus: function( v ) {
      return new Vector2( this.x - v.x, this.y - v.y );
    },
    
    minusScalar: function( scalar ) {
      return new Vector2( this.x - scalar, this.y - scalar );
    },
    
    dividedScalar: function( scalar ) {
      return new Vector2( this.x / scalar, this.y / scalar );
    },
    
    negated: function() {
      return new Vector2( -this.x, -this.y );
    },
    
    angle: function() {
      return Math.atan2( this.y, this.x );
    },
    
    // equivalent to a -PI/2 rotation (right hand rotation)
    perpendicular: function() {
      return new Vector2( this.y, -this.x );
    },
    
    angleBetween: function( v ) {
      var thisMagnitude = this.magnitude();
      var vMagnitude = v.magnitude();
      return Math.acos( dot.clamp( ( this.x * v.x + this.y * v.y ) / ( thisMagnitude * vMagnitude ), -1, 1 ) );
    },
    
    rotated: function( angle ) {
      var newAngle = this.angle() + angle;
      var mag = this.magnitude();
      return new Vector2( mag * Math.cos( newAngle ), mag * Math.sin( newAngle ) );
    },
    
    // linear interpolation from this (ratio=0) to vector (ratio=1)
    blend: function( vector, ratio ) {
      return new Vector2( this.x + (vector.x - this.x) * ratio, this.y + (vector.y - this.y) * ratio );
    },
    
    toString: function() {
      return "Vector2(" + this.x + ", " + this.y + ")";
    },
    
    toVector3: function() {
      return new dot.Vector3( this.x, this.y );
    },
    
    /*---------------------------------------------------------------------------*
     * Mutables
     *----------------------------------------------------------------------------*/
    
    set: function( x, y ) {
      this.x = x;
      this.y = y;
      return this;
    },
    
    
    setVector2: function( v ) {
      this.x = v.x;
      this.y = v.y;
      return this;
    },
    
    setX: function( x ) {
      this.x = x;
      return this;
    },
    
    setY: function( y ) {
      this.y = y;
      return this;
    },
    
    add: function( v ) {
      this.x += v.x;
      this.y += v.y;
      return this;
    },
    
    addScalar: function( scalar ) {
      this.x += scalar;
      this.y += scalar;
      return this;
    },
    
    subtract: function( v ) {
      this.x -= v.x;
      this.y -= v.y;
      return this;
    },
    
    subtractScalar: function( scalar ) {
      this.x -= scalar;
      this.y -= scalar;
      return this;
    },
    
    componentMultiply: function( v ) {
      this.x *= v.x;
      this.y *= v.y;
      return this;
    },
    
    divideScalar: function( scalar ) {
      this.x /= scalar;
      this.y /= scalar;
      return this;
    },
    
    negate: function() {
      this.x = -this.x;
      this.y = -this.y;
      return this;
    }
    
  };
  
  // experimental object pooling
  /* jshint -W064 */
  Poolable( Vector2, {
    defaultFactory: function() { return new Vector2(); },
    constructorDuplicateFactory: function( pool ) {
      return function( x, y ) {
        if ( pool.length ) {
          return pool.pop().set( x, y );
        } else {
          return new Vector2( x, y );
        }
      };
    }
  } );
  
  /*---------------------------------------------------------------------------*
   * Immutable Vector form
   *----------------------------------------------------------------------------*/
  Vector2.Immutable = function ImmutableVector2( x, y ) {
    Vector2.call( this, x, y );
  };
  var Immutable = Vector2.Immutable;
  
  inherit( Vector2, Immutable );
  
  // throw errors whenever a mutable method is called on our immutable vector
  Immutable.mutableOverrideHelper = function( mutableFunctionName ) {
    Immutable.prototype[mutableFunctionName] = function() {
      throw new Error( "Cannot call mutable method '" + mutableFunctionName + "' on immutable Vector2" );
    };
  };
  
  // TODO: better way to handle this list?
  Immutable.mutableOverrideHelper( 'set' );
  Immutable.mutableOverrideHelper( 'setX' );
  Immutable.mutableOverrideHelper( 'setY' );
  Immutable.mutableOverrideHelper( 'add' );
  Immutable.mutableOverrideHelper( 'addScalar' );
  Immutable.mutableOverrideHelper( 'subtract' );
  Immutable.mutableOverrideHelper( 'subtractScalar' );
  Immutable.mutableOverrideHelper( 'componentMultiply' );
  Immutable.mutableOverrideHelper( 'divideScalar' );
  Immutable.mutableOverrideHelper( 'negate' );
  
  // helpful immutable constants
  Vector2.ZERO = new Immutable( 0, 0 );
  Vector2.X_UNIT = new Immutable( 1, 0 );
  Vector2.Y_UNIT = new Immutable( 0, 1 );
  
  return Vector2;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Basic 4-dimensional vector
 *
 * TODO: sync with Vector2 changes
 * TODO: add quaternion extension
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Vector4',['require','ASSERT/assert','DOT/dot','DOT/Util'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'dot' );
  
  var dot = require( 'DOT/dot' );
  
  require( 'DOT/Util' );
  // require( 'DOT/Vector3' ); // commented out so Require.js doesn't complain about the circular dependency
  
  dot.Vector4 = function Vector4( x, y, z, w ) {
    // allow optional parameters
    this.x = x || 0;
    this.y = y || 0;
    this.z = z || 0;
    this.w = w !== undefined ? w : 1; // since w could be zero!
  };
  var Vector4 = dot.Vector4;
  
  Vector4.prototype = {
    constructor: Vector4,

    magnitude: function() {
      return Math.sqrt( this.magnitudeSquared() );
    },

    magnitudeSquared: function() {
      this.dot( this );
    },
    
    // the distance between this vector (treated as a point) and another point
    distance: function( point ) {
      return this.minus( point ).magnitude();
    },
    
    // the squared distance between this vector (treated as a point) and another point
    distanceSquared: function( point ) {
      return this.minus( point ).magnitudeSquared();
    },

    dot: function( v ) {
      return this.x * v.x + this.y * v.y + this.z * v.z + this.w * v.w;
    },
    
    isFinite: function() {
      return isFinite( this.x ) && isFinite( this.y ) && isFinite( this.z ) && isFinite( this.w );
    },

    /*---------------------------------------------------------------------------*
     * Immutables
     *----------------------------------------------------------------------------*/

    normalized: function() {
      var mag = this.magnitude();
      if ( mag === 0 ) {
        throw new Error( "Cannot normalize a zero-magnitude vector" );
      }
      else {
        return new Vector4( this.x / mag, this.y / mag, this.z / mag, this.w / mag );
      }
    },

    timesScalar: function( scalar ) {
      return new Vector4( this.x * scalar, this.y * scalar, this.z * scalar, this.w * scalar );
    },

    times: function( scalar ) {
      // make sure it's not a vector!
      assert && assert( scalar.dimension === undefined );
      return this.timesScalar( scalar );
    },

    componentTimes: function( v ) {
      return new Vector4( this.x * v.x, this.y * v.y, this.z * v.z, this.w * v.w );
    },

    plus: function( v ) {
      return new Vector4( this.x + v.x, this.y + v.y, this.z + v.z, this.w + v.w );
    },

    plusScalar: function( scalar ) {
      return new Vector4( this.x + scalar, this.y + scalar, this.z + scalar, this.w + scalar );
    },

    minus: function( v ) {
      return new Vector4( this.x - v.x, this.y - v.y, this.z - v.z, this.w - v.w );
    },

    minusScalar: function( scalar ) {
      return new Vector4( this.x - scalar, this.y - scalar, this.z - scalar, this.w - scalar );
    },

    dividedScalar: function( scalar ) {
      return new Vector4( this.x / scalar, this.y / scalar, this.z / scalar, this.w / scalar );
    },

    negated: function() {
      return new Vector4( -this.x, -this.y, -this.z, -this.w );
    },

    angleBetween: function( v ) {
      return Math.acos( dot.clamp( this.normalized().dot( v.normalized() ), -1, 1 ) );
    },
    
    // linear interpolation from this (ratio=0) to vector (ratio=1)
    blend: function( vector, ratio ) {
      return this.plus( vector.minus( this ).times( ratio ) );
    },

    toString: function() {
      return "Vector4(" + this.x + ", " + this.y + ", " + this.z + ", " + this.w + ")";
    },

    toVector3: function() {
      return new dot.Vector3( this.x, this.y, this.z );
    },

    /*---------------------------------------------------------------------------*
     * Mutables
     *----------------------------------------------------------------------------*/

    set: function( x, y, z, w ) {
      this.x = x;
      this.y = y;
      this.z = z;
      this.w = w;
    },

    setX: function( x ) {
      this.x = x;
    },

    setY: function( y ) {
      this.y = y;
    },

    setZ: function( z ) {
      this.z = z;
    },

    setW: function( w ) {
      this.w = w;
    },

    copy: function( v ) {
      this.x = v.x;
      this.y = v.y;
      this.z = v.z;
      this.w = v.w;
    },

    add: function( v ) {
      this.x += v.x;
      this.y += v.y;
      this.z += v.z;
      this.w += v.w;
    },

    addScalar: function( scalar ) {
      this.x += scalar;
      this.y += scalar;
      this.z += scalar;
      this.w += scalar;
    },

    subtract: function( v ) {
      this.x -= v.x;
      this.y -= v.y;
      this.z -= v.z;
      this.w -= v.w;
    },

    subtractScalar: function( scalar ) {
      this.x -= scalar;
      this.y -= scalar;
      this.z -= scalar;
      this.w -= scalar;
    },

    componentMultiply: function( v ) {
      this.x *= v.x;
      this.y *= v.y;
      this.z *= v.z;
      this.w *= v.w;
    },

    divideScalar: function( scalar ) {
      this.x /= scalar;
      this.y /= scalar;
      this.z /= scalar;
      this.w /= scalar;
    },

    negate: function() {
      this.x = -this.x;
      this.y = -this.y;
      this.z = -this.z;
      this.w = -this.w;
    },
    
    equals: function( other ) {
      return this.x === other.x && this.y === other.y && this.z === other.z && this.w === other.w;
    },
    
    equalsEpsilon: function( other, epsilon ) {
      if ( !epsilon ) {
        epsilon = 0;
      }
      return Math.abs( this.x - other.x ) + Math.abs( this.y - other.y ) + Math.abs( this.z - other.z ) + Math.abs( this.w - other.w ) <= epsilon;
    },

    isVector4: true,

    dimension: 4

  };

  /*---------------------------------------------------------------------------*
   * Immutable Vector form
   *----------------------------------------------------------------------------*/
  Vector4.Immutable = function( x, y, z, w ) {
    this.x = x || 0;
    this.y = y || 0;
    this.z = z || 0;
    this.w = w !== undefined ? w : 1;
  };
  var Immutable = Vector4.Immutable;

  Immutable.prototype = new Vector4();
  Immutable.prototype.constructor = Immutable;

  // throw errors whenever a mutable method is called on our immutable vector
  Immutable.mutableOverrideHelper = function( mutableFunctionName ) {
    Immutable.prototype[mutableFunctionName] = function() {
      throw new Error( "Cannot call mutable method '" + mutableFunctionName + "' on immutable Vector4" );
    };
  };

  // TODO: better way to handle this list?
  Immutable.mutableOverrideHelper( 'set' );
  Immutable.mutableOverrideHelper( 'setX' );
  Immutable.mutableOverrideHelper( 'setY' );
  Immutable.mutableOverrideHelper( 'setZ' );
  Immutable.mutableOverrideHelper( 'setW' );
  Immutable.mutableOverrideHelper( 'copy' );
  Immutable.mutableOverrideHelper( 'add' );
  Immutable.mutableOverrideHelper( 'addScalar' );
  Immutable.mutableOverrideHelper( 'subtract' );
  Immutable.mutableOverrideHelper( 'subtractScalar' );
  Immutable.mutableOverrideHelper( 'componentMultiply' );
  Immutable.mutableOverrideHelper( 'divideScalar' );
  Immutable.mutableOverrideHelper( 'negate' );

  // helpful immutable constants
  Vector4.ZERO = new Immutable( 0, 0, 0, 0 );
  Vector4.X_UNIT = new Immutable( 1, 0, 0, 0 );
  Vector4.Y_UNIT = new Immutable( 0, 1, 0, 0 );
  Vector4.Z_UNIT = new Immutable( 0, 0, 1, 0 );
  Vector4.W_UNIT = new Immutable( 0, 0, 0, 1 );
  
  return Vector4;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Basic 3-dimensional vector
 *
 * TODO: sync with Vector2 changes
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Vector3',['require','ASSERT/assert','DOT/dot','DOT/Util','DOT/Vector2','DOT/Vector4'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'dot' );
  
  var dot = require( 'DOT/dot' );
  
  require( 'DOT/Util' );
  require( 'DOT/Vector2' );
  require( 'DOT/Vector4' );

  dot.Vector3 = function Vector3( x, y, z ) {
    // allow optional parameters
    this.x = x || 0;
    this.y = y || 0;
    this.z = z || 0;
  };
  var Vector3 = dot.Vector3;

  Vector3.prototype = {
    constructor: Vector3,

    magnitude: function() {
      return Math.sqrt( this.magnitudeSquared() );
    },

    magnitudeSquared: function() {
      return this.dot( this );
    },
    
    // the distance between this vector (treated as a point) and another point
    distance: function( point ) {
      return this.minus( point ).magnitude();
    },
    
    // the squared distance between this vector (treated as a point) and another point
    distanceSquared: function( point ) {
      return this.minus( point ).magnitudeSquared();
    },

    dot: function( v ) {
      return this.x * v.x + this.y * v.y + this.z * v.z;
    },
    
    isFinite: function() {
      return isFinite( this.x ) && isFinite( this.y ) && isFinite( this.z );
    },

    /*---------------------------------------------------------------------------*
     * Immutables
     *----------------------------------------------------------------------------*/

    cross: function( v ) {
      return new Vector3(
          this.y * v.z - this.z * v.y,
          this.z * v.x - this.x * v.z,
          this.x * v.y - this.y * v.x
      );
    },

    normalized: function() {
      var mag = this.magnitude();
      if ( mag === 0 ) {
        throw new Error( "Cannot normalize a zero-magnitude vector" );
      }
      else {
        return new Vector3( this.x / mag, this.y / mag, this.z / mag );
      }
    },

    timesScalar: function( scalar ) {
      return new Vector3( this.x * scalar, this.y * scalar, this.z * scalar );
    },

    times: function( scalar ) {
      // make sure it's not a vector!
      assert && assert( scalar.dimension === undefined );
      return this.timesScalar( scalar );
    },

    componentTimes: function( v ) {
      return new Vector3( this.x * v.x, this.y * v.y, this.z * v.z );
    },

    plus: function( v ) {
      return new Vector3( this.x + v.x, this.y + v.y, this.z + v.z );
    },

    plusScalar: function( scalar ) {
      return new Vector3( this.x + scalar, this.y + scalar, this.z + scalar );
    },

    minus: function( v ) {
      return new Vector3( this.x - v.x, this.y - v.y, this.z - v.z );
    },

    minusScalar: function( scalar ) {
      return new Vector3( this.x - scalar, this.y - scalar, this.z - scalar );
    },

    dividedScalar: function( scalar ) {
      return new Vector3( this.x / scalar, this.y / scalar, this.z / scalar );
    },

    negated: function() {
      return new Vector3( -this.x, -this.y, -this.z );
    },

    angleBetween: function( v ) {
      return Math.acos( dot.clamp( this.normalized().dot( v.normalized() ), -1, 1 ) );
    },
    
    // linear interpolation from this (ratio=0) to vector (ratio=1)
    blend: function( vector, ratio ) {
      return this.plus( vector.minus( this ).times( ratio ) );
    },

    toString: function() {
      return "Vector3(" + this.x + ", " + this.y + ", " + this.z + ")";
    },

    toVector2: function() {
      return new dot.Vector2( this.x, this.y );
    },

    toVector4: function() {
      return new dot.Vector4( this.x, this.y, this.z );
    },

    /*---------------------------------------------------------------------------*
     * Mutables
     *----------------------------------------------------------------------------*/

    set: function( x, y, z ) {
      this.x = x;
      this.y = y;
      this.z = z;
    },

    setX: function( x ) {
      this.x = x;
    },

    setY: function( y ) {
      this.y = y;
    },

    setZ: function( z ) {
      this.z = z;
    },

    copy: function( v ) {
      this.x = v.x;
      this.y = v.y;
      this.z = v.z;
    },

    add: function( v ) {
      this.x += v.x;
      this.y += v.y;
      this.z += v.z;
    },

    addScalar: function( scalar ) {
      this.x += scalar;
      this.y += scalar;
      this.z += scalar;
    },

    subtract: function( v ) {
      this.x -= v.x;
      this.y -= v.y;
      this.z -= v.z;
    },

    subtractScalar: function( scalar ) {
      this.x -= scalar;
      this.y -= scalar;
      this.z -= scalar;
    },

    componentMultiply: function( v ) {
      this.x *= v.x;
      this.y *= v.y;
      this.z *= v.z;
    },

    divideScalar: function( scalar ) {
      this.x /= scalar;
      this.y /= scalar;
      this.z /= scalar;
    },

    negate: function() {
      this.x = -this.x;
      this.y = -this.y;
      this.z = -this.z;
    },
    
    equals: function( other ) {
      return this.x === other.x && this.y === other.y && this.z === other.z;
    },
    
    equalsEpsilon: function( other, epsilon ) {
      if ( !epsilon ) {
        epsilon = 0;
      }
      return Math.abs( this.x - other.x ) + Math.abs( this.y - other.y ) + Math.abs( this.z - other.z ) <= epsilon;
    },

    isVector3: true,

    dimension: 3

  };

  /*---------------------------------------------------------------------------*
   * Immutable Vector form
   *----------------------------------------------------------------------------*/
  Vector3.Immutable = function( x, y, z ) {
    this.x = x || 0;
    this.y = y || 0;
    this.z = z || 0;
  };
  var Immutable = Vector3.Immutable;

  Immutable.prototype = new Vector3();
  Immutable.prototype.constructor = Immutable;

  // throw errors whenever a mutable method is called on our immutable vector
  Immutable.mutableOverrideHelper = function( mutableFunctionName ) {
    Immutable.prototype[mutableFunctionName] = function() {
      throw new Error( "Cannot call mutable method '" + mutableFunctionName + "' on immutable Vector3" );
    };
  };

  // TODO: better way to handle this list?
  Immutable.mutableOverrideHelper( 'set' );
  Immutable.mutableOverrideHelper( 'setX' );
  Immutable.mutableOverrideHelper( 'setY' );
  Immutable.mutableOverrideHelper( 'setZ' );
  Immutable.mutableOverrideHelper( 'copy' );
  Immutable.mutableOverrideHelper( 'add' );
  Immutable.mutableOverrideHelper( 'addScalar' );
  Immutable.mutableOverrideHelper( 'subtract' );
  Immutable.mutableOverrideHelper( 'subtractScalar' );
  Immutable.mutableOverrideHelper( 'componentMultiply' );
  Immutable.mutableOverrideHelper( 'divideScalar' );
  Immutable.mutableOverrideHelper( 'negate' );

  // helpful immutable constants
  Vector3.ZERO = new Immutable( 0, 0, 0 );
  Vector3.X_UNIT = new Immutable( 1, 0, 0 );
  Vector3.Y_UNIT = new Immutable( 0, 1, 0 );
  Vector3.Z_UNIT = new Immutable( 0, 0, 1 );
  
  return Vector3;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * 4-dimensional Matrix
 *
 * TODO: consider adding affine flag if it will help performance (a la Matrix3)
 * TODO: get rotation angles
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Matrix4',['require','DOT/dot','DOT/Vector3','DOT/Vector4'], function( require ) {
  
  
  var dot = require( 'DOT/dot' );
  
  require( 'DOT/Vector3' );
  require( 'DOT/Vector4' );
  
  var Float32Array = window.Float32Array || Array;
  
  dot.Matrix4 = function Matrix4( v00, v01, v02, v03, v10, v11, v12, v13, v20, v21, v22, v23, v30, v31, v32, v33, type ) {

    // entries stored in column-major format
    this.entries = new Float32Array( 16 );

    this.rowMajor( v00 === undefined ? 1 : v00, v01 || 0, v02 || 0, v03 || 0,
             v10 || 0, v11 === undefined ? 1 : v11, v12 || 0, v13 || 0,
             v20 || 0, v21 || 0, v22 === undefined ? 1 : v22, v23 || 0,
             v30 || 0, v31 || 0, v32 || 0, v33 === undefined ? 1 : v33,
             type );
  };
  var Matrix4 = dot.Matrix4;

  Matrix4.Types = {
    OTHER: 0, // default
    IDENTITY: 1,
    TRANSLATION_3D: 2,
    SCALING: 3,
    AFFINE: 4

    // TODO: possibly add rotations
  };

  var Types = Matrix4.Types;

  Matrix4.identity = function() {
    return new Matrix4( 1, 0, 0, 0,
              0, 1, 0, 0,
              0, 0, 1, 0,
              0, 0, 0, 1,
              Types.IDENTITY );
  };

  Matrix4.translation = function( x, y, z ) {
    return new Matrix4( 1, 0, 0, x,
              0, 1, 0, y,
              0, 0, 1, z,
              0, 0, 0, 1,
              Types.TRANSLATION_3D );
  };

  Matrix4.translationFromVector = function( v ) { return Matrix4.translation( v.x, v.y, v.z ); };

  Matrix4.scaling = function( x, y, z ) {
    // allow using one parameter to scale everything
    y = y === undefined ? x : y;
    z = z === undefined ? x : z;

    return new Matrix4( x, 0, 0, 0,
              0, y, 0, 0,
              0, 0, z, 0,
              0, 0, 0, 1,
              Types.SCALING );
  };

  // axis is a normalized Vector3, angle in radians.
  Matrix4.rotationAxisAngle = function( axis, angle ) {
    var c = Math.cos( angle );
    var s = Math.sin( angle );
    var C = 1 - c;

    return new Matrix4( axis.x * axis.x * C + c, axis.x * axis.y * C - axis.z * s, axis.x * axis.z * C + axis.y * s, 0,
              axis.y * axis.x * C + axis.z * s, axis.y * axis.y * C + c, axis.y * axis.z * C - axis.x * s, 0,
              axis.z * axis.x * C - axis.y * s, axis.z * axis.y * C + axis.x * s, axis.z * axis.z * C + c, 0,
              0, 0, 0, 1,
              Types.AFFINE );
  };

  // TODO: add in rotation from quaternion, and from quat + translation

  Matrix4.rotationX = function( angle ) {
    var c = Math.cos( angle );
    var s = Math.sin( angle );

    return new Matrix4( 1, 0, 0, 0,
              0, c, -s, 0,
              0, s, c, 0,
              0, 0, 0, 1,
              Types.AFFINE );
  };

  Matrix4.rotationY = function( angle ) {
    var c = Math.cos( angle );
    var s = Math.sin( angle );

    return new Matrix4( c, 0, s, 0,
              0, 1, 0, 0,
              -s, 0, c, 0,
              0, 0, 0, 1,
              Types.AFFINE );
  };

  Matrix4.rotationZ = function( angle ) {
    var c = Math.cos( angle );
    var s = Math.sin( angle );

    return new Matrix4( c, -s, 0, 0,
              s, c, 0, 0,
              0, 0, 1, 0,
              0, 0, 0, 1,
              Types.AFFINE );
  };

  // aspect === width / height
  Matrix4.gluPerspective = function( fovYRadians, aspect, zNear, zFar ) {
    var cotangent = Math.cos( fovYRadians ) / Math.sin( fovYRadians );

    return new Matrix4( cotangent / aspect, 0, 0, 0,
              0, cotangent, 0, 0,
              0, 0, ( zFar + zNear ) / ( zNear - zFar ), ( 2 * zFar * zNear ) / ( zNear - zFar ),
              0, 0, -1, 0 );
  };

  Matrix4.prototype = {
    constructor: Matrix4,

    rowMajor: function( v00, v01, v02, v03, v10, v11, v12, v13, v20, v21, v22, v23, v30, v31, v32, v33, type ) {
      this.entries[0] = v00;
      this.entries[1] = v10;
      this.entries[2] = v20;
      this.entries[3] = v30;
      this.entries[4] = v01;
      this.entries[5] = v11;
      this.entries[6] = v21;
      this.entries[7] = v31;
      this.entries[8] = v02;
      this.entries[9] = v12;
      this.entries[10] = v22;
      this.entries[11] = v32;
      this.entries[12] = v03;
      this.entries[13] = v13;
      this.entries[14] = v23;
      this.entries[15] = v33;
      
      // TODO: consider performance of the affine check here
      this.type = type === undefined ? ( ( v30 === 0 && v31 === 0 && v32 === 0 && v33 === 1 ) ? Types.AFFINE : Types.OTHER ) : type;
      return this;
    },

    columnMajor: function( v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32, v03, v13, v23, v33, type ) {
      return this.rowMajor( v00, v01, v02, v03, v10, v11, v12, v13, v20, v21, v22, v23, v30, v31, v32, v33, type );
    },

    // convenience getters. inline usages of these when performance is critical? TODO: test performance of inlining these, with / without closure compiler
    m00: function() { return this.entries[0]; },
    m01: function() { return this.entries[4]; },
    m02: function() { return this.entries[8]; },
    m03: function() { return this.entries[12]; },
    m10: function() { return this.entries[1]; },
    m11: function() { return this.entries[5]; },
    m12: function() { return this.entries[9]; },
    m13: function() { return this.entries[13]; },
    m20: function() { return this.entries[2]; },
    m21: function() { return this.entries[6]; },
    m22: function() { return this.entries[10]; },
    m23: function() { return this.entries[14]; },
    m30: function() { return this.entries[3]; },
    m31: function() { return this.entries[7]; },
    m32: function() { return this.entries[11]; },
    m33: function() { return this.entries[15]; },
    
    isFinite: function() {
      return isFinite( this.m00() ) &&
             isFinite( this.m01() ) &&
             isFinite( this.m02() ) &&
             isFinite( this.m03() ) &&
             isFinite( this.m10() ) &&
             isFinite( this.m11() ) &&
             isFinite( this.m12() ) &&
             isFinite( this.m13() ) &&
             isFinite( this.m20() ) &&
             isFinite( this.m21() ) &&
             isFinite( this.m22() ) &&
             isFinite( this.m23() ) &&
             isFinite( this.m30() ) &&
             isFinite( this.m31() ) &&
             isFinite( this.m32() ) &&
             isFinite( this.m33() );
    },
    
    // the 3D translation, assuming multiplication with a homogeneous vector
    getTranslation: function() {
      return new dot.Vector3( this.m03(), this.m13(), this.m23() );
    },
    get translation() { return this.getTranslation(); },
    
    // returns a vector that is equivalent to ( T(1,0,0).magnitude(), T(0,1,0).magnitude(), T(0,0,1).magnitude() )
    // where T is a relative transform
    getScaleVector: function() {
      var m0003 = this.m00() + this.m03();
      var m1013 = this.m10() + this.m13();
      var m2023 = this.m20() + this.m23();
      var m3033 = this.m30() + this.m33();
      var m0103 = this.m01() + this.m03();
      var m1113 = this.m11() + this.m13();
      var m2123 = this.m21() + this.m23();
      var m3133 = this.m31() + this.m33();
      var m0203 = this.m02() + this.m03();
      var m1213 = this.m12() + this.m13();
      var m2223 = this.m22() + this.m23();
      var m3233 = this.m32() + this.m33();
      return new dot.Vector3( Math.sqrt( m0003 * m0003 + m1013 * m1013 + m2023 * m2023 + m3033 * m3033 ),
                              Math.sqrt( m0103 * m0103 + m1113 * m1113 + m2123 * m2123 + m3133 * m3133 ),
                              Math.sqrt( m0203 * m0203 + m1213 * m1213 + m2223 * m2223 + m3233 * m3233 ) );
    },
    get scaleVector() { return this.getScaleVector(); },
    
    getCSSTransform: function() {
      // See http://www.w3.org/TR/css3-transforms/, particularly Section 13 that discusses the SVG compatibility
      
      // we need to prevent the numbers from being in an exponential toString form, since the CSS transform does not support that
      // 20 is the largest guaranteed number of digits according to https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toFixed
      
      // the inner part of a CSS3 transform, but remember to add the browser-specific parts!
      // NOTE: the toFixed calls are inlined for performance reasons
      return 'matrix3d(' + this.entries[0].toFixed( 20 ) + ',' +
                           this.entries[1].toFixed( 20 ) + ',' +
                           this.entries[2].toFixed( 20 ) + ',' +
                           this.entries[3].toFixed( 20 ) + ',' +
                           this.entries[4].toFixed( 20 ) + ',' +
                           this.entries[5].toFixed( 20 ) + ',' +
                           this.entries[6].toFixed( 20 ) + ',' +
                           this.entries[7].toFixed( 20 ) + ',' +
                           this.entries[8].toFixed( 20 ) + ',' +
                           this.entries[9].toFixed( 20 ) + ',' +
                           this.entries[10].toFixed( 20 ) + ',' +
                           this.entries[11].toFixed( 20 ) + ',' +
                           this.entries[12].toFixed( 20 ) + ',' +
                           this.entries[13].toFixed( 20 ) + ',' +
                           this.entries[14].toFixed( 20 ) + ',' +
                           this.entries[15].toFixed( 20 ) + ')';
    },
    get cssTransform() { return this.getCSSTransform(); },
    
    // exact equality
    equals: function( m ) {
      return this.m00() === m.m00() && this.m01() === m.m01() && this.m02() === m.m02() && this.m03() === m.m03() &&
             this.m10() === m.m10() && this.m11() === m.m11() && this.m12() === m.m12() && this.m13() === m.m13() &&
             this.m20() === m.m20() && this.m21() === m.m21() && this.m22() === m.m22() && this.m23() === m.m23() &&
             this.m30() === m.m30() && this.m31() === m.m31() && this.m32() === m.m32() && this.m33() === m.m33();
    },
    
    // equality within a margin of error
    equalsEpsilon: function( m, epsilon ) {
      return Math.abs( this.m00() - m.m00() ) < epsilon &&
             Math.abs( this.m01() - m.m01() ) < epsilon &&
             Math.abs( this.m02() - m.m02() ) < epsilon &&
             Math.abs( this.m03() - m.m03() ) < epsilon &&
             Math.abs( this.m10() - m.m10() ) < epsilon &&
             Math.abs( this.m11() - m.m11() ) < epsilon &&
             Math.abs( this.m12() - m.m12() ) < epsilon &&
             Math.abs( this.m13() - m.m13() ) < epsilon &&
             Math.abs( this.m20() - m.m20() ) < epsilon &&
             Math.abs( this.m21() - m.m21() ) < epsilon &&
             Math.abs( this.m22() - m.m22() ) < epsilon &&
             Math.abs( this.m23() - m.m23() ) < epsilon &&
             Math.abs( this.m30() - m.m30() ) < epsilon &&
             Math.abs( this.m31() - m.m31() ) < epsilon &&
             Math.abs( this.m32() - m.m32() ) < epsilon &&
             Math.abs( this.m33() - m.m33() ) < epsilon;
    },
    
    /*---------------------------------------------------------------------------*
    * Immutable operations (returning a new matrix)
    *----------------------------------------------------------------------------*/
    
    copy: function() {
      return new Matrix4(
        this.m00(), this.m01(), this.m02(), this.m03(),
        this.m10(), this.m11(), this.m12(), this.m13(),
        this.m20(), this.m21(), this.m22(), this.m23(),
        this.m30(), this.m31(), this.m32(), this.m33(),
        this.type
      );
    },
    
    plus: function( m ) {
      return new Matrix4(
          this.m00() + m.m00(), this.m01() + m.m01(), this.m02() + m.m02(), this.m03() + m.m03(),
          this.m10() + m.m10(), this.m11() + m.m11(), this.m12() + m.m12(), this.m13() + m.m13(),
          this.m20() + m.m20(), this.m21() + m.m21(), this.m22() + m.m22(), this.m23() + m.m23(),
          this.m30() + m.m30(), this.m31() + m.m31(), this.m32() + m.m32(), this.m33() + m.m33()
      );
    },

    minus: function( m ) {
      return new Matrix4(
          this.m00() - m.m00(), this.m01() - m.m01(), this.m02() - m.m02(), this.m03() - m.m03(),
          this.m10() - m.m10(), this.m11() - m.m11(), this.m12() - m.m12(), this.m13() - m.m13(),
          this.m20() - m.m20(), this.m21() - m.m21(), this.m22() - m.m22(), this.m23() - m.m23(),
          this.m30() - m.m30(), this.m31() - m.m31(), this.m32() - m.m32(), this.m33() - m.m33()
      );
    },

    transposed: function() {
      return new Matrix4( this.m00(), this.m10(), this.m20(), this.m30(),
                this.m01(), this.m11(), this.m21(), this.m31(),
                this.m02(), this.m12(), this.m22(), this.m32(),
                this.m03(), this.m13(), this.m23(), this.m33() );
    },

    negated: function() {
      return new Matrix4( -this.m00(), -this.m01(), -this.m02(), -this.m03(),
                -this.m10(), -this.m11(), -this.m12(), -this.m13(),
                -this.m20(), -this.m21(), -this.m22(), -this.m23(),
                -this.m30(), -this.m31(), -this.m32(), -this.m33() );
    },

    inverted: function() {
      switch ( this.type ) {
        case Types.IDENTITY:
          return this;
        case Types.TRANSLATION_3D:
          return new Matrix4( 1, 0, 0, -this.m03(),
                              0, 1, 0, -this.m13(),
                              0, 0, 1, -this.m23(),
                              0, 0, 0, 1, Types.TRANSLATION_3D );
        case Types.SCALING:
          return new Matrix4( 1 / this.m00(), 0, 0, 0,
                              0, 1 / this.m11(), 0, 0,
                              0, 0, 1 / this.m22(), 0,
                              0, 0, 0, 1 / this.m33(), Types.SCALING );
        case Types.AFFINE:
        case Types.OTHER:
          var det = this.getDeterminant();
          if ( det !== 0 ) {
            return new Matrix4(
                ( -this.m31() * this.m22() * this.m13() + this.m21() * this.m32() * this.m13() + this.m31() * this.m12() * this.m23() - this.m11() * this.m32() * this.m23() - this.m21() * this.m12() * this.m33() + this.m11() * this.m22() * this.m33() ) / det,
                ( this.m31() * this.m22() * this.m03() - this.m21() * this.m32() * this.m03() - this.m31() * this.m02() * this.m23() + this.m01() * this.m32() * this.m23() + this.m21() * this.m02() * this.m33() - this.m01() * this.m22() * this.m33() ) / det,
                ( -this.m31() * this.m12() * this.m03() + this.m11() * this.m32() * this.m03() + this.m31() * this.m02() * this.m13() - this.m01() * this.m32() * this.m13() - this.m11() * this.m02() * this.m33() + this.m01() * this.m12() * this.m33() ) / det,
                ( this.m21() * this.m12() * this.m03() - this.m11() * this.m22() * this.m03() - this.m21() * this.m02() * this.m13() + this.m01() * this.m22() * this.m13() + this.m11() * this.m02() * this.m23() - this.m01() * this.m12() * this.m23() ) / det,
                ( this.m30() * this.m22() * this.m13() - this.m20() * this.m32() * this.m13() - this.m30() * this.m12() * this.m23() + this.m10() * this.m32() * this.m23() + this.m20() * this.m12() * this.m33() - this.m10() * this.m22() * this.m33() ) / det,
                ( -this.m30() * this.m22() * this.m03() + this.m20() * this.m32() * this.m03() + this.m30() * this.m02() * this.m23() - this.m00() * this.m32() * this.m23() - this.m20() * this.m02() * this.m33() + this.m00() * this.m22() * this.m33() ) / det,
                ( this.m30() * this.m12() * this.m03() - this.m10() * this.m32() * this.m03() - this.m30() * this.m02() * this.m13() + this.m00() * this.m32() * this.m13() + this.m10() * this.m02() * this.m33() - this.m00() * this.m12() * this.m33() ) / det,
                ( -this.m20() * this.m12() * this.m03() + this.m10() * this.m22() * this.m03() + this.m20() * this.m02() * this.m13() - this.m00() * this.m22() * this.m13() - this.m10() * this.m02() * this.m23() + this.m00() * this.m12() * this.m23() ) / det,
                ( -this.m30() * this.m21() * this.m13() + this.m20() * this.m31() * this.m13() + this.m30() * this.m11() * this.m23() - this.m10() * this.m31() * this.m23() - this.m20() * this.m11() * this.m33() + this.m10() * this.m21() * this.m33() ) / det,
                ( this.m30() * this.m21() * this.m03() - this.m20() * this.m31() * this.m03() - this.m30() * this.m01() * this.m23() + this.m00() * this.m31() * this.m23() + this.m20() * this.m01() * this.m33() - this.m00() * this.m21() * this.m33() ) / det,
                ( -this.m30() * this.m11() * this.m03() + this.m10() * this.m31() * this.m03() + this.m30() * this.m01() * this.m13() - this.m00() * this.m31() * this.m13() - this.m10() * this.m01() * this.m33() + this.m00() * this.m11() * this.m33() ) / det,
                ( this.m20() * this.m11() * this.m03() - this.m10() * this.m21() * this.m03() - this.m20() * this.m01() * this.m13() + this.m00() * this.m21() * this.m13() + this.m10() * this.m01() * this.m23() - this.m00() * this.m11() * this.m23() ) / det,
                ( this.m30() * this.m21() * this.m12() - this.m20() * this.m31() * this.m12() - this.m30() * this.m11() * this.m22() + this.m10() * this.m31() * this.m22() + this.m20() * this.m11() * this.m32() - this.m10() * this.m21() * this.m32() ) / det,
                ( -this.m30() * this.m21() * this.m02() + this.m20() * this.m31() * this.m02() + this.m30() * this.m01() * this.m22() - this.m00() * this.m31() * this.m22() - this.m20() * this.m01() * this.m32() + this.m00() * this.m21() * this.m32() ) / det,
                ( this.m30() * this.m11() * this.m02() - this.m10() * this.m31() * this.m02() - this.m30() * this.m01() * this.m12() + this.m00() * this.m31() * this.m12() + this.m10() * this.m01() * this.m32() - this.m00() * this.m11() * this.m32() ) / det,
                ( -this.m20() * this.m11() * this.m02() + this.m10() * this.m21() * this.m02() + this.m20() * this.m01() * this.m12() - this.m00() * this.m21() * this.m12() - this.m10() * this.m01() * this.m22() + this.m00() * this.m11() * this.m22() ) / det
            );
          } else {
            throw new Error( 'Matrix could not be inverted, determinant === 0' );
          }
          break; // because JSHint totally can't tell that this can't be reached
        default:
          throw new Error( 'Matrix3.inverted with unknown type: ' + this.type );
      }
    },

    timesMatrix: function( m ) {
      // I * M === M * I === I (the identity)
      if( this.type === Types.IDENTITY || m.type === Types.IDENTITY ) {
        return this.type === Types.IDENTITY ? m : this;
      }
      
      if ( this.type === m.type ) {
        // currently two matrices of the same type will result in the same result type
        if ( this.type === Types.TRANSLATION_3D ) {
          // faster combination of translations
          return new Matrix4( 1, 0, 0, this.m03() + m.m02(),
                              0, 1, 0, this.m13() + m.m12(),
                              0, 0, 1, this.m23() + m.m23(),
                              0, 0, 0, 1, Types.TRANSLATION_3D );
        } else if ( this.type === Types.SCALING ) {
          // faster combination of scaling
          return new Matrix4( this.m00() * m.m00(), 0, 0, 0,
                              0, this.m11() * m.m11(), 0, 0,
                              0, 0, this.m22() * m.m22(), 0,
                              0, 0, 0, 1, Types.SCALING );
        }
      }
      
      if ( this.type !== Types.OTHER && m.type !== Types.OTHER ) {
        // currently two matrices that are anything but "other" are technically affine, and the result will be affine
        
        // affine case
        return new Matrix4( this.m00() * m.m00() + this.m01() * m.m10() + this.m02() * m.m20(),
                            this.m00() * m.m01() + this.m01() * m.m11() + this.m02() * m.m21(),
                            this.m00() * m.m02() + this.m01() * m.m12() + this.m02() * m.m22(),
                            this.m00() * m.m03() + this.m01() * m.m13() + this.m02() * m.m23() + this.m03(),
                            this.m10() * m.m00() + this.m11() * m.m10() + this.m12() * m.m20(),
                            this.m10() * m.m01() + this.m11() * m.m11() + this.m12() * m.m21(),
                            this.m10() * m.m02() + this.m11() * m.m12() + this.m12() * m.m22(),
                            this.m10() * m.m03() + this.m11() * m.m13() + this.m12() * m.m23() + this.m13(),
                            this.m20() * m.m00() + this.m21() * m.m10() + this.m22() * m.m20(),
                            this.m20() * m.m01() + this.m21() * m.m11() + this.m22() * m.m21(),
                            this.m20() * m.m02() + this.m21() * m.m12() + this.m22() * m.m22(),
                            this.m20() * m.m03() + this.m21() * m.m13() + this.m22() * m.m23() + this.m23(),
                            0, 0, 0, 1, Types.AFFINE );
      }
      
      // general case
      return new Matrix4( this.m00() * m.m00() + this.m01() * m.m10() + this.m02() * m.m20() + this.m03() * m.m30(),
                this.m00() * m.m01() + this.m01() * m.m11() + this.m02() * m.m21() + this.m03() * m.m31(),
                this.m00() * m.m02() + this.m01() * m.m12() + this.m02() * m.m22() + this.m03() * m.m32(),
                this.m00() * m.m03() + this.m01() * m.m13() + this.m02() * m.m23() + this.m03() * m.m33(),
                this.m10() * m.m00() + this.m11() * m.m10() + this.m12() * m.m20() + this.m13() * m.m30(),
                this.m10() * m.m01() + this.m11() * m.m11() + this.m12() * m.m21() + this.m13() * m.m31(),
                this.m10() * m.m02() + this.m11() * m.m12() + this.m12() * m.m22() + this.m13() * m.m32(),
                this.m10() * m.m03() + this.m11() * m.m13() + this.m12() * m.m23() + this.m13() * m.m33(),
                this.m20() * m.m00() + this.m21() * m.m10() + this.m22() * m.m20() + this.m23() * m.m30(),
                this.m20() * m.m01() + this.m21() * m.m11() + this.m22() * m.m21() + this.m23() * m.m31(),
                this.m20() * m.m02() + this.m21() * m.m12() + this.m22() * m.m22() + this.m23() * m.m32(),
                this.m20() * m.m03() + this.m21() * m.m13() + this.m22() * m.m23() + this.m23() * m.m33(),
                this.m30() * m.m00() + this.m31() * m.m10() + this.m32() * m.m20() + this.m33() * m.m30(),
                this.m30() * m.m01() + this.m31() * m.m11() + this.m32() * m.m21() + this.m33() * m.m31(),
                this.m30() * m.m02() + this.m31() * m.m12() + this.m32() * m.m22() + this.m33() * m.m32(),
                this.m30() * m.m03() + this.m31() * m.m13() + this.m32() * m.m23() + this.m33() * m.m33() );
    },

    timesVector4: function( v ) {
      var x = this.m00() * v.x + this.m01() * v.y + this.m02() * v.z + this.m03() * v.w;
      var y = this.m10() * v.x + this.m11() * v.y + this.m12() * v.z + this.m13() * v.w;
      var z = this.m20() * v.x + this.m21() * v.y + this.m22() * v.z + this.m23() * v.w;
      var w = this.m30() * v.x + this.m31() * v.y + this.m32() * v.z + this.m33() * v.w;
      return new dot.Vector4( x, y, z, w );
    },

    timesVector3: function( v ) {
      return this.timesVector4( v.toVector4() ).toVector3();
    },

    timesTransposeVector4: function( v ) {
      var x = this.m00() * v.x + this.m10() * v.y + this.m20() * v.z + this.m30() * v.w;
      var y = this.m01() * v.x + this.m11() * v.y + this.m21() * v.z + this.m31() * v.w;
      var z = this.m02() * v.x + this.m12() * v.y + this.m22() * v.z + this.m32() * v.w;
      var w = this.m03() * v.x + this.m13() * v.y + this.m23() * v.z + this.m33() * v.w;
      return new dot.Vector4( x, y, z, w );
    },

    timesTransposeVector3: function( v ) {
      return this.timesTransposeVector4( v.toVector4() ).toVector3();
    },

    timesRelativeVector3: function( v ) {
      var x = this.m00() * v.x + this.m10() * v.y + this.m20() * v.z;
      var y = this.m01() * v.y + this.m11() * v.y + this.m21() * v.z;
      var z = this.m02() * v.z + this.m12() * v.y + this.m22() * v.z;
      return new dot.Vector3( x, y, z );
    },

    getDeterminant: function() {
      return this.m03() * this.m12() * this.m21() * this.m30() -
          this.m02() * this.m13() * this.m21() * this.m30() -
          this.m03() * this.m11() * this.m22() * this.m30() +
          this.m01() * this.m13() * this.m22() * this.m30() +
          this.m02() * this.m11() * this.m23() * this.m30() -
          this.m01() * this.m12() * this.m23() * this.m30() -
          this.m03() * this.m12() * this.m20() * this.m31() +
          this.m02() * this.m13() * this.m20() * this.m31() +
          this.m03() * this.m10() * this.m22() * this.m31() -
          this.m00() * this.m13() * this.m22() * this.m31() -
          this.m02() * this.m10() * this.m23() * this.m31() +
          this.m00() * this.m12() * this.m23() * this.m31() +
          this.m03() * this.m11() * this.m20() * this.m32() -
          this.m01() * this.m13() * this.m20() * this.m32() -
          this.m03() * this.m10() * this.m21() * this.m32() +
          this.m00() * this.m13() * this.m21() * this.m32() +
          this.m01() * this.m10() * this.m23() * this.m32() -
          this.m00() * this.m11() * this.m23() * this.m32() -
          this.m02() * this.m11() * this.m20() * this.m33() +
          this.m01() * this.m12() * this.m20() * this.m33() +
          this.m02() * this.m10() * this.m21() * this.m33() -
          this.m00() * this.m12() * this.m21() * this.m33() -
          this.m01() * this.m10() * this.m22() * this.m33() +
          this.m00() * this.m11() * this.m22() * this.m33();
    },
    get determinant() { return this.getDeterminant(); },

    toString: function() {
      return this.m00() + " " + this.m01() + " " + this.m02() + " " + this.m03() + "\n" +
           this.m10() + " " + this.m11() + " " + this.m12() + " " + this.m13() + "\n" +
           this.m20() + " " + this.m21() + " " + this.m22() + " " + this.m23() + "\n" +
           this.m30() + " " + this.m31() + " " + this.m32() + " " + this.m33();
    },

    makeImmutable: function() {
      this.rowMajor = function() {
        throw new Error( "Cannot modify immutable matrix" );
      };
    }
  };

  // create an immutable
  Matrix4.IDENTITY = new Matrix4();
  Matrix4.IDENTITY.makeImmutable();
  
  return Matrix4;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * 3-dimensional Matrix
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Matrix3',['require','DOT/dot','DOT/Vector2','DOT/Vector3','DOT/Matrix4'], function( require ) {
  
  
  var dot = require( 'DOT/dot' );
  
  var FastArray = dot.FastArray;
  
  require( 'DOT/Vector2' );
  require( 'DOT/Vector3' );
  require( 'DOT/Matrix4' );
  
  dot.Matrix3 = function Matrix3( v00, v01, v02, v10, v11, v12, v20, v21, v22, type ) {

    // entries stored in column-major format
    this.entries = new FastArray( 9 ); // TODO: consider a typed array if possible (double even?) for performance and compatibility with WebGL

    this.rowMajor( v00 === undefined ? 1 : v00, v01 || 0, v02 || 0,
                   v10 || 0, v11 === undefined ? 1 : v11, v12 || 0,
                   v20 || 0, v21 || 0, v22 === undefined ? 1 : v22,
                   type );
    
    phetAllocation && phetAllocation( 'Matrix3' );
  };
  var Matrix3 = dot.Matrix3;

  Matrix3.Types = {
    // NOTE: if an inverted matrix of a type is not that type, change inverted()!
    // NOTE: if two matrices with identical types are multiplied, the result should have the same type. if not, changed timesMatrix()!
    // NOTE: on adding a type, exaustively check all type usage
    OTHER: 0, // default
    IDENTITY: 1,
    TRANSLATION_2D: 2,
    SCALING: 3,
    AFFINE: 4

    // TODO: possibly add rotations
  };

  var Types = Matrix3.Types;

  Matrix3.identity = function() {
    return new Matrix3( 1, 0, 0,
                        0, 1, 0,
                        0, 0, 1,
                        Types.IDENTITY );
  };

  Matrix3.translation = function( x, y ) {
    return new Matrix3( 1, 0, x,
                        0, 1, y,
                        0, 0, 1,
                        Types.TRANSLATION_2D );
  };

  Matrix3.translationFromVector = function( v ) { return Matrix3.translation( v.x, v.y ); };

  Matrix3.scaling = function( x, y ) {
    // allow using one parameter to scale everything
    y = y === undefined ? x : y;

    return new Matrix3( x, 0, 0,
                        0, y, 0,
                        0, 0, 1,
                        Types.SCALING );
  };
  Matrix3.scale = Matrix3.scaling;
  
  Matrix3.affine = function( m00, m10, m01, m11, m02, m12 ) {
    return new Matrix3( m00, m01, m02, m10, m11, m12, 0, 0, 1, Types.AFFINE );
  };

  // axis is a normalized Vector3, angle in radians.
  Matrix3.rotationAxisAngle = function( axis, angle ) {
    var c = Math.cos( angle );
    var s = Math.sin( angle );
    var C = 1 - c;

    return new Matrix3( axis.x * axis.x * C + c, axis.x * axis.y * C - axis.z * s, axis.x * axis.z * C + axis.y * s,
                        axis.y * axis.x * C + axis.z * s, axis.y * axis.y * C + c, axis.y * axis.z * C - axis.x * s,
                        axis.z * axis.x * C - axis.y * s, axis.z * axis.y * C + axis.x * s, axis.z * axis.z * C + c,
                        Types.OTHER );
  };

  // TODO: add in rotation from quaternion, and from quat + translation

  Matrix3.rotationX = function( angle ) {
    var c = Math.cos( angle );
    var s = Math.sin( angle );

    return new Matrix3( 1, 0, 0,
                        0, c, -s,
                        0, s, c,
                        Types.OTHER );
  };

  Matrix3.rotationY = function( angle ) {
    var c = Math.cos( angle );
    var s = Math.sin( angle );

    return new Matrix3( c, 0, s,
                        0, 1, 0,
                        -s, 0, c,
                        Types.OTHER );
  };

  Matrix3.rotationZ = function( angle ) {
    var c = Math.cos( angle );
    var s = Math.sin( angle );

    return new Matrix3( c, -s, 0,
                        s, c, 0,
                        0, 0, 1,
                        Types.AFFINE );
  };
  
  // standard 2d rotation
  Matrix3.rotation2 = Matrix3.rotationZ;
  
  Matrix3.fromSVGMatrix = function( svgMatrix ) {
    return new Matrix3( svgMatrix.a, svgMatrix.c, svgMatrix.e,
                        svgMatrix.b, svgMatrix.d, svgMatrix.f,
                        0, 0, 1,
                        Types.AFFINE );
  };

  // a rotation matrix that rotates A to B, by rotating about the axis A.cross( B ) -- Shortest path. ideally should be unit vectors
  Matrix3.rotateAToB = function( a, b ) {
    // see http://graphics.cs.brown.edu/~jfh/papers/Moller-EBA-1999/paper.pdf for information on this implementation
    var start = a;
    var end = b;

    var epsilon = 0.0001;

    var e, h, f;

    var v = start.cross( end );
    e = start.dot( end );
    f = ( e < 0 ) ? -e : e;

    // if "from" and "to" vectors are nearly parallel
    if ( f > 1.0 - epsilon ) {
      var c1, c2, c3;
      /* coefficients for later use */
      var i, j;

      var x = new dot.Vector3(
        ( start.x > 0.0 ) ? start.x : -start.x,
        ( start.y > 0.0 ) ? start.y : -start.y,
        ( start.z > 0.0 ) ? start.z : -start.z
      );

      if ( x.x < x.y ) {
        if ( x.x < x.z ) {
          x = dot.Vector3.X_UNIT;
        }
        else {
          x = dot.Vector3.Z_UNIT;
        }
      }
      else {
        if ( x.y < x.z ) {
          x = dot.Vector3.Y_UNIT;
        }
        else {
          x = dot.Vector3.Z_UNIT;
        }
      }

      var u = x.minus( start );
      v = x.minus( end );

      c1 = 2.0 / u.dot( u );
      c2 = 2.0 / v.dot( v );
      c3 = c1 * c2 * u.dot( v );

      return Matrix3.IDENTITY.plus( Matrix3.rowMajor(
        -c1 * u.x * u.x - c2 * v.x * v.x + c3 * v.x * u.x,
        -c1 * u.x * u.y - c2 * v.x * v.y + c3 * v.x * u.y,
        -c1 * u.x * u.z - c2 * v.x * v.z + c3 * v.x * u.z,
        -c1 * u.y * u.x - c2 * v.y * v.x + c3 * v.y * u.x,
        -c1 * u.y * u.y - c2 * v.y * v.y + c3 * v.y * u.y,
        -c1 * u.y * u.z - c2 * v.y * v.z + c3 * v.y * u.z,
        -c1 * u.z * u.x - c2 * v.z * v.x + c3 * v.z * u.x,
        -c1 * u.z * u.y - c2 * v.z * v.y + c3 * v.z * u.y,
        -c1 * u.z * u.z - c2 * v.z * v.z + c3 * v.z * u.z
      ) );
    }
    else {
      // the most common case, unless "start"="end", or "start"=-"end"
      var hvx, hvz, hvxy, hvxz, hvyz;
      h = 1.0 / ( 1.0 + e );
      hvx = h * v.x;
      hvz = h * v.z;
      hvxy = hvx * v.y;
      hvxz = hvx * v.z;
      hvyz = hvz * v.y;

      return Matrix3.rowMajor(
        e + hvx * v.x, hvxy - v.z, hvxz + v.y,
        hvxy + v.z, e + h * v.y * v.y, hvyz - v.x,
        hvxz - v.y, hvyz + v.x, e + hvz * v.z
      );
    }
  };

  Matrix3.prototype = {
    constructor: Matrix3,
    
    /*---------------------------------------------------------------------------*
    * "Properties"
    *----------------------------------------------------------------------------*/
    
    // convenience getters. inline usages of these when performance is critical? TODO: test performance of inlining these, with / without closure compiler
    m00: function() { return this.entries[0]; },
    m01: function() { return this.entries[3]; },
    m02: function() { return this.entries[6]; },
    m10: function() { return this.entries[1]; },
    m11: function() { return this.entries[4]; },
    m12: function() { return this.entries[7]; },
    m20: function() { return this.entries[2]; },
    m21: function() { return this.entries[5]; },
    m22: function() { return this.entries[8]; },
    
    isAffine: function() {
      return this.type === Types.AFFINE || ( this.m20() === 0 && this.m21() === 0 && this.m22() === 1 );
    },
    
    isFinite: function() {
      return isFinite( this.m00() ) &&
             isFinite( this.m01() ) &&
             isFinite( this.m02() ) &&
             isFinite( this.m10() ) &&
             isFinite( this.m11() ) &&
             isFinite( this.m12() ) &&
             isFinite( this.m20() ) &&
             isFinite( this.m21() ) &&
             isFinite( this.m22() );
    },
    
    getDeterminant: function() {
      return this.m00() * this.m11() * this.m22() + this.m01() * this.m12() * this.m20() + this.m02() * this.m10() * this.m21() - this.m02() * this.m11() * this.m20() - this.m01() * this.m10() * this.m22() - this.m00() * this.m12() * this.m21();
    },
    get determinant() { return this.getDeterminant(); },
    
    // the 2D translation, assuming multiplication with a homogeneous vector
    getTranslation: function() {
      return new dot.Vector2( this.m02(), this.m12() );
    },
    get translation() { return this.getTranslation(); },
    
    // returns a vector that is equivalent to ( T(1,0).magnitude(), T(0,1).magnitude() ) where T is a relative transform
    getScaleVector: function() {
      return new dot.Vector2( Math.sqrt( this.m00() * this.m00() + this.m10() * this.m10() ),
                              Math.sqrt( this.m01() * this.m01() + this.m11() * this.m11() ) );
    },
    get scaleVector() { return this.getScaleVector(); },
    
    // angle in radians for the 2d rotation from this matrix, between pi, -pi
    getRotation: function() {
      var transformedVector = this.timesVector2( dot.Vector2.X_UNIT ).minus( this.timesVector2( dot.Vector2.ZERO ) );
      return Math.atan2( transformedVector.y, transformedVector.x );
    },
    get rotation() { return this.getRotation(); },
    
    toMatrix4: function() {
      return new dot.Matrix4( this.m00(), this.m01(), this.m02(), 0,
                              this.m10(), this.m11(), this.m12(), 0,
                              this.m20(), this.m21(), this.m22(), 0,
                              0, 0, 0, 1 );
    },
    
    toString: function() {
      return this.m00() + ' ' + this.m01() + ' ' + this.m02() + '\n' +
             this.m10() + ' ' + this.m11() + ' ' + this.m12() + '\n' +
             this.m20() + ' ' + this.m21() + ' ' + this.m22();
    },
    
    toSVGMatrix: function() {
      var result = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ).createSVGMatrix();
      
      // top two rows
      result.a = this.m00();
      result.b = this.m10();
      result.c = this.m01();
      result.d = this.m11();
      result.e = this.m02();
      result.f = this.m12();
      
      return result;
    },
    
    getCSSTransform: function() {
      // See http://www.w3.org/TR/css3-transforms/, particularly Section 13 that discusses the SVG compatibility
      
      // we need to prevent the numbers from being in an exponential toString form, since the CSS transform does not support that
      // 20 is the largest guaranteed number of digits according to https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toFixed
      
      // the inner part of a CSS3 transform, but remember to add the browser-specific parts!
      // NOTE: the toFixed calls are inlined for performance reasons
      return 'matrix(' + this.entries[0].toFixed( 20 ) + ',' + this.entries[1].toFixed( 20 ) + ',' + this.entries[3].toFixed( 20 ) + ',' + this.entries[4].toFixed( 20 ) + ',' + this.entries[6].toFixed( 20 ) + ',' + this.entries[7].toFixed( 20 ) + ')';
    },
    get cssTransform() { return this.getCSSTransform(); },
    
    getSVGTransform: function() {
      // SVG transform presentation attribute. See http://www.w3.org/TR/SVG/coords.html#TransformAttribute
      
      // we need to prevent the numbers from being in an exponential toString form, since the CSS transform does not support that
      function svgNumber( number ) {
        // largest guaranteed number of digits according to https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toFixed
        return number.toFixed( 20 );
      }
      
      switch( this.type ) {
        case Types.IDENTITY:
          return '';
        case Types.TRANSLATION_2D:
          return 'translate(' + svgNumber( this.entries[6] ) + ',' + this.entries[7] + ')';
        case Types.SCALING:
          return 'scale(' + svgNumber( this.entries[0] ) + ( this.entries[0] === this.entries[4] ? '' : ',' + svgNumber( this.entries[4] ) ) + ')';
        default:
          return 'matrix(' + svgNumber( this.entries[0] ) + ',' + svgNumber( this.entries[1] ) + ',' + svgNumber( this.entries[3] ) + ',' + svgNumber( this.entries[4] ) + ',' + svgNumber( this.entries[6] ) + ',' + svgNumber( this.entries[7] ) + ')';
      }
    },
    get svgTransform() { return this.getSVGTransform(); },
    
    // returns a parameter object suitable for use with jQuery's .css()
    getCSSTransformStyles: function() {
      var transformCSS = this.getCSSTransform();
      
      // notes on triggering hardware acceleration: http://creativejs.com/2011/12/day-2-gpu-accelerate-your-dom-elements/
      return {
        // force iOS hardware acceleration
        '-webkit-perspective': 1000,
        '-webkit-backface-visibility': 'hidden',
        
        '-webkit-transform': transformCSS + ' translateZ(0)', // trigger hardware acceleration if possible
        '-moz-transform': transformCSS + ' translateZ(0)', // trigger hardware acceleration if possible
        '-ms-transform': transformCSS,
        '-o-transform': transformCSS,
        'transform': transformCSS,
        'transform-origin': 'top left', // at the origin of the component. consider 0px 0px instead. Critical, since otherwise this defaults to 50% 50%!!! see https://developer.mozilla.org/en-US/docs/CSS/transform-origin
        '-ms-transform-origin': 'top left' // TODO: do we need other platform-specific transform-origin styles?
      };
    },
    get cssTransformStyles() { return this.getCSSTransformStyles(); },
    
    // exact equality
    equals: function( m ) {
      return this.m00() === m.m00() && this.m01() === m.m01() && this.m02() === m.m02() &&
             this.m10() === m.m10() && this.m11() === m.m11() && this.m12() === m.m12() &&
             this.m20() === m.m20() && this.m21() === m.m21() && this.m22() === m.m22();
    },
    
    // equality within a margin of error
    equalsEpsilon: function( m, epsilon ) {
      return Math.abs( this.m00() - m.m00() ) < epsilon && Math.abs( this.m01() - m.m01() ) < epsilon && Math.abs( this.m02() - m.m02() ) < epsilon &&
             Math.abs( this.m10() - m.m10() ) < epsilon && Math.abs( this.m11() - m.m11() ) < epsilon && Math.abs( this.m12() - m.m12() ) < epsilon &&
             Math.abs( this.m20() - m.m20() ) < epsilon && Math.abs( this.m21() - m.m21() ) < epsilon && Math.abs( this.m22() - m.m22() ) < epsilon;
    },
    
    /*---------------------------------------------------------------------------*
    * Immutable operations (returns a new matrix)
    *----------------------------------------------------------------------------*/
    
    copy: function() {
      return new Matrix3(
        this.m00(), this.m01(), this.m02(),
        this.m10(), this.m11(), this.m12(),
        this.m20(), this.m21(), this.m22(),
        this.type
      );
    },
    
    plus: function( m ) {
      return new Matrix3(
        this.m00() + m.m00(), this.m01() + m.m01(), this.m02() + m.m02(),
        this.m10() + m.m10(), this.m11() + m.m11(), this.m12() + m.m12(),
        this.m20() + m.m20(), this.m21() + m.m21(), this.m22() + m.m22()
      );
    },
    
    minus: function( m ) {
      return new Matrix3(
        this.m00() - m.m00(), this.m01() - m.m01(), this.m02() - m.m02(),
        this.m10() - m.m10(), this.m11() - m.m11(), this.m12() - m.m12(),
        this.m20() - m.m20(), this.m21() - m.m21(), this.m22() - m.m22()
      );
    },
    
    transposed: function() {
      return new Matrix3(
        this.m00(), this.m10(), this.m20(),
        this.m01(), this.m11(), this.m21(),
        this.m02(), this.m12(), this.m22(), ( this.type === Types.IDENTITY || this.type === Types.SCALING ) ? this.type : undefined
      );
    },
    
    negated: function() {
      return new Matrix3(
        -this.m00(), -this.m01(), -this.m02(),
        -this.m10(), -this.m11(), -this.m12(),
        -this.m20(), -this.m21(), -this.m22()
      );
    },
    
    inverted: function() {
      var det;
      
      switch ( this.type ) {
        case Types.IDENTITY:
          return this;
        case Types.TRANSLATION_2D:
          return new Matrix3( 1, 0, -this.m02(),
                              0, 1, -this.m12(),
                              0, 0, 1, Types.TRANSLATION_2D );
        case Types.SCALING:
          return new Matrix3( 1 / this.m00(), 0, 0,
                              0, 1 / this.m11(), 0,
                              0, 0, 1 / this.m22(), Types.SCALING );
        case Types.AFFINE:
          det = this.getDeterminant();
          if ( det !== 0 ) {
            return new Matrix3(
              ( -this.m12() * this.m21() + this.m11() * this.m22() ) / det,
              ( this.m02() * this.m21() - this.m01() * this.m22() ) / det,
              ( -this.m02() * this.m11() + this.m01() * this.m12() ) / det,
              ( this.m12() * this.m20() - this.m10() * this.m22() ) / det,
              ( -this.m02() * this.m20() + this.m00() * this.m22() ) / det,
              ( this.m02() * this.m10() - this.m00() * this.m12() ) / det,
              0, 0, 1, Types.AFFINE
            );
          } else {
            throw new Error( 'Matrix could not be inverted, determinant === 0' );
          }
          break; // because JSHint totally can't tell that this can't be reached
        case Types.OTHER:
          det = this.getDeterminant();
          if ( det !== 0 ) {
            return new Matrix3(
              ( -this.m12() * this.m21() + this.m11() * this.m22() ) / det,
              ( this.m02() * this.m21() - this.m01() * this.m22() ) / det,
              ( -this.m02() * this.m11() + this.m01() * this.m12() ) / det,
              ( this.m12() * this.m20() - this.m10() * this.m22() ) / det,
              ( -this.m02() * this.m20() + this.m00() * this.m22() ) / det,
              ( this.m02() * this.m10() - this.m00() * this.m12() ) / det,
              ( -this.m11() * this.m20() + this.m10() * this.m21() ) / det,
              ( this.m01() * this.m20() - this.m00() * this.m21() ) / det,
              ( -this.m01() * this.m10() + this.m00() * this.m11() ) / det,
              Types.OTHER
            );
          } else {
            throw new Error( 'Matrix could not be inverted, determinant === 0' );
          }
          break; // because JSHint totally can't tell that this can't be reached
        default:
          throw new Error( 'Matrix3.inverted with unknown type: ' + this.type );
      }
    },
    
    timesMatrix: function( m ) {
      // I * M === M * I === I (the identity)
      if( this.type === Types.IDENTITY || m.type === Types.IDENTITY ) {
        return this.type === Types.IDENTITY ? m : this;
      }
      
      if ( this.type === m.type ) {
        // currently two matrices of the same type will result in the same result type
        if ( this.type === Types.TRANSLATION_2D ) {
          // faster combination of translations
          return new Matrix3( 1, 0, this.m02() + m.m02(),
                              0, 1, this.m12() + m.m12(),
                              0, 0, 1, Types.TRANSLATION_2D );
        } else if ( this.type === Types.SCALING ) {
          // faster combination of scaling
          return new Matrix3( this.m00() * m.m00(), 0, 0,
                              0, this.m11() * m.m11(), 0,
                              0, 0, 1, Types.SCALING );
        }
      }
      
      if ( this.type !== Types.OTHER && m.type !== Types.OTHER ) {
        // currently two matrices that are anything but "other" are technically affine, and the result will be affine
        
        // affine case
        return new Matrix3( this.m00() * m.m00() + this.m01() * m.m10(),
                            this.m00() * m.m01() + this.m01() * m.m11(),
                            this.m00() * m.m02() + this.m01() * m.m12() + this.m02(),
                            this.m10() * m.m00() + this.m11() * m.m10(),
                            this.m10() * m.m01() + this.m11() * m.m11(),
                            this.m10() * m.m02() + this.m11() * m.m12() + this.m12(),
                            0, 0, 1, Types.AFFINE );
      }
      
      // general case
      return new Matrix3( this.m00() * m.m00() + this.m01() * m.m10() + this.m02() * m.m20(),
                          this.m00() * m.m01() + this.m01() * m.m11() + this.m02() * m.m21(),
                          this.m00() * m.m02() + this.m01() * m.m12() + this.m02() * m.m22(),
                          this.m10() * m.m00() + this.m11() * m.m10() + this.m12() * m.m20(),
                          this.m10() * m.m01() + this.m11() * m.m11() + this.m12() * m.m21(),
                          this.m10() * m.m02() + this.m11() * m.m12() + this.m12() * m.m22(),
                          this.m20() * m.m00() + this.m21() * m.m10() + this.m22() * m.m20(),
                          this.m20() * m.m01() + this.m21() * m.m11() + this.m22() * m.m21(),
                          this.m20() * m.m02() + this.m21() * m.m12() + this.m22() * m.m22() );
    },
    
    /*---------------------------------------------------------------------------*
    * Immutable operations (returns new form of a parameter)
    *----------------------------------------------------------------------------*/
    
    timesVector2: function( v ) {
      var x = this.m00() * v.x + this.m01() * v.y + this.m02();
      var y = this.m10() * v.x + this.m11() * v.y + this.m12();
      return new dot.Vector2( x, y );
    },
    
    timesVector3: function( v ) {
      var x = this.m00() * v.x + this.m01() * v.y + this.m02() * v.z;
      var y = this.m10() * v.x + this.m11() * v.y + this.m12() * v.z;
      var z = this.m20() * v.x + this.m21() * v.y + this.m22() * v.z;
      return new dot.Vector3( x, y, z );
    },
    
    timesTransposeVector2: function( v ) {
      var x = this.m00() * v.x + this.m10() * v.y;
      var y = this.m01() * v.x + this.m11() * v.y;
      return new dot.Vector2( x, y );
    },
    
    // TODO: this operation seems to not work for transformDelta2, should be vetted
    timesRelativeVector2: function( v ) {
      var x = this.m00() * v.x + this.m01() * v.y;
      var y = this.m10() * v.y + this.m11() * v.y;
      return new dot.Vector2( x, y );
    },
    
    /*---------------------------------------------------------------------------*
    * Mutable operations (changes this matrix)
    *----------------------------------------------------------------------------*/
    
    makeImmutable: function() {
      this.rowMajor = function() {
        throw new Error( 'Cannot modify immutable matrix' );
      };
      return this;
    },
    
    rowMajor: function( v00, v01, v02, v10, v11, v12, v20, v21, v22, type ) {
      this.entries[0] = v00;
      this.entries[1] = v10;
      this.entries[2] = v20;
      this.entries[3] = v01;
      this.entries[4] = v11;
      this.entries[5] = v21;
      this.entries[6] = v02;
      this.entries[7] = v12;
      this.entries[8] = v22;
      
      // TODO: consider performance of the affine check here
      this.type = type === undefined ? ( ( v20 === 0 && v21 === 0 && v22 === 1 ) ? Types.AFFINE : Types.OTHER ) : type;
      return this;
    },
    
    columnMajor: function( v00, v10, v20, v01, v11, v21, v02, v12, v22, type ) {
      return this.rowMajor( v00, v01, v02, v10, v11, v12, v20, v21, v22, type );
    },
    
    add: function( m ) {
      return this.rowMajor(
        this.m00() + m.m00(), this.m01() + m.m01(), this.m02() + m.m02(),
        this.m10() + m.m10(), this.m11() + m.m11(), this.m12() + m.m12(),
        this.m20() + m.m20(), this.m21() + m.m21(), this.m22() + m.m22()
      );
    },
    
    subtract: function( m ) {
      return this.rowMajor(
        this.m00() - m.m00(), this.m01() - m.m01(), this.m02() - m.m02(),
        this.m10() - m.m10(), this.m11() - m.m11(), this.m12() - m.m12(),
        this.m20() - m.m20(), this.m21() - m.m21(), this.m22() - m.m22()
      );
    },
    
    transpose: function() {
      return this.rowMajor(
        this.m00(), this.m10(), this.m20(),
        this.m01(), this.m11(), this.m21(),
        this.m02(), this.m12(), this.m22(),
        ( this.type === Types.IDENTITY || this.type === Types.SCALING ) ? this.type : undefined
      );
    },
    
    negate: function() {
      return this.rowMajor(
        -this.m00(), -this.m01(), -this.m02(),
        -this.m10(), -this.m11(), -this.m12(),
        -this.m20(), -this.m21(), -this.m22()
      );
    },
    
    invert: function() {
      var det;
      
      switch ( this.type ) {
        case Types.IDENTITY:
          return this;
        case Types.TRANSLATION_2D:
          return this.rowMajor( 1, 0, -this.m02(),
                                0, 1, -this.m12(),
                                0, 0, 1, Types.TRANSLATION_2D );
        case Types.SCALING:
          return this.rowMajor( 1 / this.m00(), 0, 0,
                                0, 1 / this.m11(), 0,
                                0, 0, 1 / this.m22(), Types.SCALING );
        case Types.AFFINE:
          det = this.getDeterminant();
          if ( det !== 0 ) {
            return this.rowMajor(
              ( -this.m12() * this.m21() + this.m11() * this.m22() ) / det,
              ( this.m02() * this.m21() - this.m01() * this.m22() ) / det,
              ( -this.m02() * this.m11() + this.m01() * this.m12() ) / det,
              ( this.m12() * this.m20() - this.m10() * this.m22() ) / det,
              ( -this.m02() * this.m20() + this.m00() * this.m22() ) / det,
              ( this.m02() * this.m10() - this.m00() * this.m12() ) / det,
              0, 0, 1, Types.AFFINE
            );
          } else {
            throw new Error( 'Matrix could not be inverted, determinant === 0' );
          }
          break; // because JSHint totally can't tell that this can't be reached
        case Types.OTHER:
          det = this.getDeterminant();
          if ( det !== 0 ) {
            return this.rowMajor(
              ( -this.m12() * this.m21() + this.m11() * this.m22() ) / det,
              ( this.m02() * this.m21() - this.m01() * this.m22() ) / det,
              ( -this.m02() * this.m11() + this.m01() * this.m12() ) / det,
              ( this.m12() * this.m20() - this.m10() * this.m22() ) / det,
              ( -this.m02() * this.m20() + this.m00() * this.m22() ) / det,
              ( this.m02() * this.m10() - this.m00() * this.m12() ) / det,
              ( -this.m11() * this.m20() + this.m10() * this.m21() ) / det,
              ( this.m01() * this.m20() - this.m00() * this.m21() ) / det,
              ( -this.m01() * this.m10() + this.m00() * this.m11() ) / det,
              Types.OTHER
            );
          } else {
            throw new Error( 'Matrix could not be inverted, determinant === 0' );
          }
          break; // because JSHint totally can't tell that this can't be reached
        default:
          throw new Error( 'Matrix3.inverted with unknown type: ' + this.type );
      }
    },
    
    multiplyMatrix: function( m ) {
      // I * M === M * I === I (the identity)
      if( this.type === Types.IDENTITY || m.type === Types.IDENTITY ) {
        return this.type === Types.IDENTITY ? m : this;
      }
      
      if ( this.type === m.type ) {
        // currently two matrices of the same type will result in the same result type
        if ( this.type === Types.TRANSLATION_2D ) {
          // faster combination of translations
          return this.rowMajor( 1, 0, this.m02() + m.m02(),
                                0, 1, this.m12() + m.m12(),
                                0, 0, 1, Types.TRANSLATION_2D );
        } else if ( this.type === Types.SCALING ) {
          // faster combination of scaling
          return this.rowMajor( this.m00() * m.m00(), 0, 0,
                                0, this.m11() * m.m11(), 0,
                                0, 0, 1, Types.SCALING );
        }
      }
      
      if ( this.type !== Types.OTHER && m.type !== Types.OTHER ) {
        // currently two matrices that are anything but "other" are technically affine, and the result will be affine
        
        // affine case
        return this.rowMajor( this.m00() * m.m00() + this.m01() * m.m10(),
                              this.m00() * m.m01() + this.m01() * m.m11(),
                              this.m00() * m.m02() + this.m01() * m.m12() + this.m02(),
                              this.m10() * m.m00() + this.m11() * m.m10(),
                              this.m10() * m.m01() + this.m11() * m.m11(),
                              this.m10() * m.m02() + this.m11() * m.m12() + this.m12(),
                              0, 0, 1, Types.AFFINE );
      }
      
      // general case
      return this.rowMajor( this.m00() * m.m00() + this.m01() * m.m10() + this.m02() * m.m20(),
                            this.m00() * m.m01() + this.m01() * m.m11() + this.m02() * m.m21(),
                            this.m00() * m.m02() + this.m01() * m.m12() + this.m02() * m.m22(),
                            this.m10() * m.m00() + this.m11() * m.m10() + this.m12() * m.m20(),
                            this.m10() * m.m01() + this.m11() * m.m11() + this.m12() * m.m21(),
                            this.m10() * m.m02() + this.m11() * m.m12() + this.m12() * m.m22(),
                            this.m20() * m.m00() + this.m21() * m.m10() + this.m22() * m.m20(),
                            this.m20() * m.m01() + this.m21() * m.m11() + this.m22() * m.m21(),
                            this.m20() * m.m02() + this.m21() * m.m12() + this.m22() * m.m22() );
    },
    
    /*---------------------------------------------------------------------------*
    * Mutable operations (changes the parameter)
    *----------------------------------------------------------------------------*/
    
    multiplyVector2: function( v ) {
      var x = this.m00() * v.x + this.m01() * v.y + this.m02();
      var y = this.m10() * v.x + this.m11() * v.y + this.m12();
      v.setX( x );
      v.setY( y );
      return v;
    },
    
    multiplyVector3: function( v ) {
      var x = this.m00() * v.x + this.m01() * v.y + this.m02() * v.z;
      var y = this.m10() * v.x + this.m11() * v.y + this.m12() * v.z;
      var z = this.m20() * v.x + this.m21() * v.y + this.m22() * v.z;
      v.setX( x );
      v.setY( y );
      v.setZ( z );
      return v;
    },
    
    multiplyTransposeVector2: function( v ) {
      var x = this.m00() * v.x + this.m10() * v.y;
      var y = this.m01() * v.x + this.m11() * v.y;
      v.setX( x );
      v.setY( y );
      return v;
    },
    
    multiplyRelativeVector2: function( v ) {
      var x = this.m00() * v.x + this.m01() * v.y;
      var y = this.m10() * v.y + this.m11() * v.y;
      v.setX( x );
      v.setY( y );
      return v;
    },
    
    // sets the transform of a Canvas 2D rendering context to the affine part of this matrix
    canvasSetTransform: function( context ) {
      context.setTransform(
        // inlined array entries
        this.entries[0],
        this.entries[1],
        this.entries[3],
        this.entries[4],
        this.entries[6],
        this.entries[7]
      );
    },
    
    // appends the affine part of this matrix to the Canvas 2D rendering context
    canvasAppendTransform: function( context ) {
      if ( this.type !== Types.IDENTITY ) {
        context.transform(
          // inlined array entries
          this.entries[0],
          this.entries[1],
          this.entries[3],
          this.entries[4],
          this.entries[6],
          this.entries[7]
        );
      }
    }
  };
  
  // create an immutable
  Matrix3.IDENTITY = new Matrix3( 1, 0, 0,
                                  0, 1, 0,
                                  0, 0, 1,
                                  Types.IDENTITY );
  Matrix3.IDENTITY.makeImmutable();
  
  Matrix3.X_REFLECTION = new Matrix3( -1, 0, 0,
                                       0, 1, 0,
                                       0, 0, 1,
                                       Types.AFFINE );
  Matrix3.X_REFLECTION.makeImmutable();
  
  Matrix3.Y_REFLECTION = new Matrix3( 1,  0, 0,
                                      0, -1, 0,
                                      0,  0, 1,
                                      Types.AFFINE );
  Matrix3.Y_REFLECTION.makeImmutable();
  
  //Shortcut for translation times a matrix (without allocating a translation matrix), see scenery#119
  Matrix3.translationTimesMatrix = function( x, y, m ) {
    var type;
    if ( m.type === Types.IDENTITY || m.type === Types.TRANSLATION_2D ) {
      return new Matrix3( 1, 0, m.m02() + x,
                          0, 1, m.m12() + y,
                          0, 0, 1,
                          Types.TRANSLATION_2D );
    } else if ( m.type === Types.OTHER ) {
      type = Types.OTHER;
    } else {
      type = Types.AFFINE;
    }
    return new Matrix3( m.m00(), m.m01(), m.m02() + x,
                        m.m10(), m.m11(), m.m12() + y,
                        m.m20(), m.m21(), m.m22(),
                        type );
  };
  
  Matrix3.printer = {
    print: function( matrix ) {
      console.log( matrix.toString() );
    }
  };
  
  return Matrix3;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * 2-dimensional ray
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Ray2',['require','ASSERT/assert','DOT/dot'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'dot' );
  
  var dot = require( 'DOT/dot' );

  dot.Ray2 = function Ray2( pos, dir ) {
    this.pos = pos;
    this.dir = dir;
    
    assert && assert( Math.abs( dir.magnitude() - 1 ) < 0.01 );
    
    phetAllocation && phetAllocation( 'Ray2' );
  };
  var Ray2 = dot.Ray2;

  Ray2.prototype = {
    constructor: Ray2,

    shifted: function( distance ) {
      return new Ray2( this.pointAtDistance( distance ), this.dir );
    },

    pointAtDistance: function( distance ) {
      return this.pos.plus( this.dir.timesScalar( distance ) );
    },

    toString: function() {
      return this.pos.toString() + " => " + this.dir.toString();
    }
  };
  
  return Ray2;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Forward and inverse transforms with 3x3 matrices
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Transform3',['require','ASSERT/assert','DOT/dot','DOT/Matrix3','DOT/Vector2','DOT/Ray2'], function( require ) {
  

  var assert = require( 'ASSERT/assert' )( 'dot' );
  
  var dot = require( 'DOT/dot' );
  
  require( 'DOT/Matrix3' );
  require( 'DOT/Vector2' );
  require( 'DOT/Ray2' );

  // takes a 4x4 matrix
  dot.Transform3 = function Transform3( matrix ) {
    this.listeners = [];
    
    // using immutable version for now. change it to the mutable identity copy if we need mutable operations on the matrices
    this.set( matrix === undefined ? dot.Matrix3.IDENTITY : matrix );
    
    phetAllocation && phetAllocation( 'Transform3' );
  };
  var Transform3 = dot.Transform3;

  Transform3.prototype = {
    constructor: Transform3,
    
    /*---------------------------------------------------------------------------*
    * mutators
    *----------------------------------------------------------------------------*/
    
    set: function( matrix ) {
      // TODO: performance: don't notify or handle instances where the matrix is detected to be the identity matrix?
      assert && assert( matrix instanceof dot.Matrix3 );
      
      var oldMatrix = this.matrix;
      var length = this.listeners.length;
      var i;
      
      // notify listeners before the change
      for ( i = 0; i < length; i++ ) {
        this.listeners[i].before( matrix, oldMatrix );
      }
      
      this.matrix = matrix;
      
      // compute these lazily
      this.inverse = null;
      this.matrixTransposed = null;
      this.inverseTransposed = null;
      
      // notify listeners after the change
      for ( i = 0; i < length; i++ ) {
        this.listeners[i].after( matrix, oldMatrix );
      }
    },
    
    prepend: function( matrix ) {
      this.set( matrix.timesMatrix( this.matrix ) );
    },

    //Simpler case of prepending a translation without having to allocate a matrix for it, see scenery#119
    prependTranslation: function( x, y ) {
      this.set( dot.Matrix3.translationTimesMatrix( x, y, this.matrix ) );
    },

    append: function( matrix ) {
      this.set( this.matrix.timesMatrix( matrix ) );
    },

    prependTransform: function( transform ) {
      this.prepend( transform.matrix );
    },

    appendTransform: function( transform ) {
      this.append( transform.matrix );
    },

    applyToCanvasContext: function( context ) {
      context.setTransform( this.matrix.m00(), this.matrix.m10(), this.matrix.m01(), this.matrix.m11(), this.matrix.m02(), this.matrix.m12() );
    },
    
    /*---------------------------------------------------------------------------*
    * getters
    *----------------------------------------------------------------------------*/
    
    // uses the same matrices, for use cases where the matrices are considered immutable
    copy: function() {
      var transform = new Transform3( this.matrix );
      transform.inverse = this.inverse;
      transform.matrixTransposed = this.matrixTransposed;
      transform.inverseTransposed = this.inverseTransposed;
    },
    
    // copies matrices, for use cases where the matrices are considered mutable
    deepCopy: function() {
      var transform = new Transform3( this.matrix.copy() );
      transform.inverse = this.inverse ? this.inverse.copy() : null;
      transform.matrixTransposed = this.matrixTransposed ? this.matrixTransposed.copy() : null;
      transform.inverseTransposed = this.inverseTransposed ? this.inverseTransposed.copy() : null;
    },
    
    getMatrix: function() {
      return this.matrix;
    },
    
    getInverse: function() {
      if ( this.inverse === null ) {
        this.inverse = this.matrix.inverted();
      }
      return this.inverse;
    },
    
    getMatrixTransposed: function() {
      if ( this.matrixTransposed === null ) {
        this.matrixTransposed = this.matrix.transposed();
      }
      return this.matrixTransposed;
    },
    
    getInverseTransposed: function() {
      if ( this.inverseTransposed === null ) {
        this.inverseTransposed = this.getInverse().transposed();
      }
      return this.inverseTransposed;
    },
    
    isIdentity: function() {
      return this.matrix.type === dot.Matrix3.Types.IDENTITY;
    },
    
    isFinite: function() {
      return this.matrix.isFinite();
    },

    /*---------------------------------------------------------------------------*
     * forward transforms (for Vector2 or scalar)
     *----------------------------------------------------------------------------*/

    // transform a position (includes translation)
    transformPosition2: function( vec2 ) {
      return this.matrix.timesVector2( vec2 );
    },

    // transform a vector (exclude translation)
    transformDelta2: function( vec2 ) {
      var m = this.getMatrix();
      // m . vec2 - m . Vector2.ZERO
      return new dot.Vector2( m.m00() * vec2.x + m.m01() * vec2.y, m.m10() * vec2.x + m.m11() * vec2.y );
    },

    // transform a normal vector (different than a normal vector)
    transformNormal2: function( vec2 ) {
      return this.getInverse().timesTransposeVector2( vec2 );
    },
    
    transformX: function( x ) {
      var m = this.getMatrix();
      // TODO: ensure assertions are stripped out
      assert && assert( !m.m01(), 'Transforming an X value with a rotation/shear is ill-defined' );
      return m.m00() * x + m.m02();
    },
    
    transformY: function( y ) {
      var m = this.getMatrix();
      assert && assert( !m.m10(), 'Transforming a Y value with a rotation/shear is ill-defined' );
      return m.m11() * y + m.m12();
    },
    
    transformDeltaX: function( x ) {
      var m = this.getMatrix();
      assert && assert( !m.m01(), 'Transforming an X value with a rotation/shear is ill-defined' );
      // same as this.transformDelta2( new dot.Vector2( x, 0 ) ).x;
      return m.m00() * x;
    },
    
    transformDeltaY: function( y ) {
      var m = this.getMatrix();
      assert && assert( !m.m10(), 'Transforming a Y value with a rotation/shear is ill-defined' );
      // same as this.transformDelta2( new dot.Vector2( 0, y ) ).y;
      return m.m11() * y;
    },
    
    transformBounds2: function( bounds2 ) {
      return bounds2.transformed( this.matrix );
    },
    
    transformShape: function( shape ) {
      return shape.transformed( this.matrix );
    },
    
    transformRay2: function( ray ) {
      return new dot.Ray2( this.transformPosition2( ray.pos ), this.transformDelta2( ray.dir ).normalized() );
    },

    /*---------------------------------------------------------------------------*
     * inverse transforms (for Vector2 or scalar)
     *----------------------------------------------------------------------------*/

    inversePosition2: function( vec2 ) {
      return this.getInverse().timesVector2( vec2 );
    },

    inverseDelta2: function( vec2 ) {
      var m = this.getInverse();
      // m . vec2 - m . Vector2.ZERO
      return new dot.Vector2( m.m00() * vec2.x + m.m01() * vec2.y, m.m10() * vec2.x + m.m11() * vec2.y );
    },

    inverseNormal2: function( vec2 ) {
      return this.matrix.timesTransposeVector2( vec2 );
    },
    
    inverseX: function( x ) {
      var m = this.getInverse();
      assert && assert( !m.m01(), 'Inverting an X value with a rotation/shear is ill-defined' );
      return m.m00() * x + m.m02();
    },
    
    inverseY: function( y ) {
      var m = this.getInverse();
      assert && assert( !m.m10(), 'Inverting a Y value with a rotation/shear is ill-defined' );
      return m.m11() * y + m.m12();
    },
    
    inverseDeltaX: function( x ) {
      var m = this.getInverse();
      assert && assert( !m.m01(), 'Inverting an X value with a rotation/shear is ill-defined' );
      // same as this.inverseDelta2( new dot.Vector2( x, 0 ) ).x;
      return m.m00() * x;
    },

    inverseDeltaY: function( y ) {
      var m = this.getInverse();
      assert && assert( !m.m10(), 'Inverting a Y value with a rotation/shear is ill-defined' );
      // same as this.inverseDelta2( new dot.Vector2( 0, y ) ).y;
      return m.m11() * y;
    },
    
    inverseBounds2: function( bounds2 ) {
      return bounds2.transformed( this.getInverse() );
    },
    
    inverseShape: function( shape ) {
      return shape.transformed( this.getInverse() );
    },
    
    inverseRay2: function( ray ) {
      return new dot.Ray2( this.inversePosition2( ray.pos ), this.inverseDelta2( ray.dir ).normalized() );
    },
    
    /*---------------------------------------------------------------------------*
    * listeners
    *----------------------------------------------------------------------------*/
    
    // note: listener.before( matrix, oldMatrix ) will be called before the change, listener.after( matrix, oldMatrix ) will be called after
    addTransformListener: function( listener ) {
      assert && assert( !_.contains( this.listeners, listener ) );
      this.listeners.push( listener );
    },
    
    // useful for making sure the listener is triggered first
    prependTransformListener: function( listener ) {
      assert && assert( !_.contains( this.listeners, listener ) );
      this.listeners.unshift( listener );
    },
    
    removeTransformListener: function( listener ) {
      assert && assert( _.contains( this.listeners, listener ) );
      this.listeners.splice( _.indexOf( this.listeners, listener ), 1 );
    }
  };
  
  return Transform3;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * A 2D rectangle-shaped bounded area (bounding box)
 *
 * There are a number of convenience functions to get locations and points on the Bounds. Currently we do not
 * store these with the Bounds2 instance, since we want to lower the memory footprint.
 *
 * minX, minY, maxX, and maxY are actually stored. We don't do x,y,width,height because this can't properly express
 * semi-infinite bounds (like a half-plane), or easily handle what Bounds2.NOTHING and Bounds2.EVERYTHING do with
 * the constructive solid areas.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Bounds2',['require','ASSERT/assert','DOT/dot','DOT/Vector2'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'dot' );
  
  var dot = require( 'DOT/dot' );
  
  require( 'DOT/Vector2' );
  
  // not using x,y,width,height so that it can handle infinity-based cases in a better way
  dot.Bounds2 = function Bounds2( minX, minY, maxX, maxY ) {
    assert && assert( maxY !== undefined, 'Bounds2 requires 4 parameters' );
    this.minX = minX;
    this.minY = minY;
    this.maxX = maxX;
    this.maxY = maxY;
    
    phetAllocation && phetAllocation( 'Bounds2' );
  };
  var Bounds2 = dot.Bounds2;

  Bounds2.prototype = {
    constructor: Bounds2,
    
    /*---------------------------------------------------------------------------*
    * Properties
    *----------------------------------------------------------------------------*/
    
    getWidth: function() { return this.maxX - this.minX; },
    get width() { return this.getWidth(); },
    
    getHeight: function() { return this.maxY - this.minY; },
    get height() { return this.getHeight(); },
    
    /* 
     * Convenience locations
     * upper is in terms of the visual layout in Scenery and other programs, so the minY is the "upper", and minY is the "lower"
     *
     *             minX (x)     centerX        maxX
     *          ---------------------------------------
     * minY (y) | upperLeft   upperCenter   upperRight
     * centerY  | centerLeft    center      centerRight
     * maxY     | lowerLeft   lowerCenter   lowerRight
     */
    getX: function() { return this.minX; },
    get x() { return this.getX(); },
    getY: function() { return this.minY; },
    get y() { return this.getY(); },
    
    getMinX: function() { return this.minX; },
    get left() { return this.minX; },
    getMinY: function() { return this.minY; },
    get top() { return this.minY; },
    getMaxX: function() { return this.maxX; },
    get right() { return this.maxX; },
    getMaxY: function() { return this.maxY; },
    get bottom() { return this.maxY; },
    
    getCenterX: function() { return ( this.maxX + this.minX ) / 2; },
    get centerX() { return this.getCenterX(); },
    getCenterY: function() { return ( this.maxY + this.minY ) / 2; },
    get centerY() { return this.getCenterY(); },
    
    getUpperLeft: function() { return new dot.Vector2( this.minX, this.minY ); },
    get upperLeft() { return this.getUpperLeft(); },
    getUpperCenter: function() { return new dot.Vector2( this.getCenterX(), this.minY ); },
    get upperCenter() { return this.getUpperCenter(); },
    getUpperRight: function() { return new dot.Vector2( this.maxX, this.minY ); },
    get upperRight() { return this.getUpperRight(); },
    getCenterLeft: function() { return new dot.Vector2( this.minX, this.getCenterY ); },
    get centerLeft() { return this.getCenterLeft(); },
    getCenter: function() { return new dot.Vector2( this.getCenterX(), this.getCenterY() ); },
    get center() { return this.getCenter(); },
    getCenterRight: function() { return new dot.Vector2( this.maxX, this.getCenterY ); },
    get centerRight() { return this.getCenterRight(); },
    getLowerLeft: function() { return new dot.Vector2( this.minX, this.maxY ); },
    get lowerLeft() { return this.getLowerLeft(); },
    getLowerCenter: function() { return new dot.Vector2( this.getCenterX(), this.maxY ); },
    get lowerCenter() { return this.getLowerCenter(); },
    getLowerRight: function() { return new dot.Vector2( this.maxX, this.maxY ); },
    get lowerRight() { return this.getLowerRight(); },
    
    isEmpty: function() { return this.getWidth() < 0 || this.getHeight() < 0; },
    
    isFinite: function() {
      return isFinite( this.minX ) && isFinite( this.minY ) && isFinite( this.maxX ) && isFinite( this.maxY );
    },
    
    isValid: function() {
      return !this.isEmpty() && this.isFinite();
    },
    
    // whether the coordinates are inside the bounding box (or on the boundary)
    containsCoordinates: function( x, y ) {
      return this.minX <= x && x <= this.maxX && this.minY <= y && y <= this.maxY;
    },
    
    // whether the point is inside the bounding box (or on the boundary)
    containsPoint: function( point ) {
      return this.containsCoordinates( point.x, point.y );
    },
    
    // whether this bounding box completely contains the argument bounding box
    containsBounds: function( bounds ) {
      return this.minX <= bounds.minX && this.maxX >= bounds.maxX && this.minY <= bounds.minY && this.maxY >= bounds.maxY;
    },
    
    // whether the intersection is non-empty (if they share any part of a boundary, this will be true)
    intersectsBounds: function( bounds ) {
      // TODO: more efficient way of doing this?
      return !this.intersection( bounds ).isEmpty();
    },
    
    toString: function() {
      return '[x:(' + this.minX + ',' + this.maxX + '),y:(' + this.minY + ',' + this.maxY + ')]';
    },
    
    equals: function( other ) {
      return this.minX === other.minX && this.minY === other.minY && this.maxX === other.maxX && this.maxY === other.maxY;
    },
    
    equalsEpsilon: function( other, epsilon ) {
      epsilon = epsilon || 0;
      var thisFinite = this.isFinite();
      var otherFinite = other.isFinite();
      if ( thisFinite && otherFinite ) {
        // both are finite, so we can use Math.abs() - it would fail with non-finite values like Infinity
        return Math.abs( this.minX - other.minX ) < epsilon &&
               Math.abs( this.minY - other.minY ) < epsilon &&
               Math.abs( this.maxX - other.maxX ) < epsilon &&
               Math.abs( this.maxY - other.maxY ) < epsilon;
      } else if ( thisFinite !== otherFinite ) {
        return false; // one is finite, the other is not. definitely not equal
      } else if ( this === other ) {
        return true; // exact same instance, must be equal
      } else {
        // epsilon only applies on finite dimensions. due to JS's handling of isFinite(), it's faster to check the sum of both
        return ( isFinite( this.minX + other.minX ) ? ( Math.abs( this.minX - other.minX ) < epsilon ) : ( this.minX === other.minX ) ) &&
               ( isFinite( this.minY + other.minY ) ? ( Math.abs( this.minY - other.minY ) < epsilon ) : ( this.minY === other.minY ) ) &&
               ( isFinite( this.maxX + other.maxX ) ? ( Math.abs( this.maxX - other.maxX ) < epsilon ) : ( this.maxX === other.maxX ) ) &&
               ( isFinite( this.maxY + other.maxY ) ? ( Math.abs( this.maxY - other.maxY ) < epsilon ) : ( this.maxY === other.maxY ) );
      }
    },
    
    /*---------------------------------------------------------------------------*
    * Immutable operations
    *----------------------------------------------------------------------------*/
    
    copy: function() {
      return new Bounds2( this.minX, this.minY, this.maxX, this.maxY );
    },
    
    // immutable operations (bounding-box style handling, so that the relevant bounds contain everything)
    union: function( bounds ) {
      return new Bounds2(
        Math.min( this.minX, bounds.minX ),
        Math.min( this.minY, bounds.minY ),
        Math.max( this.maxX, bounds.maxX ),
        Math.max( this.maxY, bounds.maxY )
      );
    },
    intersection: function( bounds ) {
      return new Bounds2(
        Math.max( this.minX, bounds.minX ),
        Math.max( this.minY, bounds.minY ),
        Math.min( this.maxX, bounds.maxX ),
        Math.min( this.maxY, bounds.maxY )
      );
    },
    // TODO: difference should be well-defined, but more logic is needed to compute
    
    withCoordinates: function( x, y ) {
      return new Bounds2(
        Math.min( this.minX, x ),
        Math.min( this.minY, y ),
        Math.max( this.maxX, x ),
        Math.max( this.maxY, y )
      );
    },
    
    // like a union with a point-sized bounding box
    withPoint: function( point ) {
      return this.withCoordinates( point.x, point.y );
    },
    
    withMinX: function( minX ) { return new Bounds2( minX, this.minY, this.maxX, this.maxY ); },
    withMinY: function( minY ) { return new Bounds2( this.minX, minY, this.maxX, this.maxY ); },
    withMaxX: function( maxX ) { return new Bounds2( this.minX, this.minY, maxX, this.maxY ); },
    withMaxY: function( maxY ) { return new Bounds2( this.minX, this.minY, this.maxX, maxY ); },
    
    // copy rounded to integral values, expanding where necessary
    roundedOut: function() {
      return new Bounds2(
        Math.floor( this.minX ),
        Math.floor( this.minY ),
        Math.ceil( this.maxX ),
        Math.ceil( this.maxY )
      );
    },
    
    // copy rounded to integral values, contracting where necessary
    roundedIn: function() {
      return new Bounds2(
        Math.ceil( this.minX ),
        Math.ceil( this.minY ),
        Math.floor( this.maxX ),
        Math.floor( this.maxY )
      );
    },
    
    // transform a bounding box.
    // NOTE that box.transformed( matrix ).transformed( inverse ) may be larger than the original box
    transformed: function( matrix ) {
      return this.copy().transform( matrix );
    },
    
    // returns copy expanded on all sides by length d
    dilated: function( d ) {
      return new Bounds2( this.minX - d, this.minY - d, this.maxX + d, this.maxY + d );
    },
    
    // returns copy contracted on all sides by length d
    eroded: function( d ) {
      return this.dilated( -d );
    },
    
    shiftedX: function( x ) {
      return new Bounds2( this.minX + x, this.minY, this.maxX + x, this.maxY );
    },
    
    shiftedY: function( y ) {
      return new Bounds2( this.minX, this.minY + y, this.maxX, this.maxY + y );
    },
    
    shifted: function( x, y ) {
      return new Bounds2( this.minX + x, this.minY + y, this.maxX + x, this.maxY + y );
    },
    
    /*---------------------------------------------------------------------------*
    * Mutable operations
    *----------------------------------------------------------------------------*/
    
    set: function( minX, minY, maxX, maxY ) {
      this.minX = minX;
      this.minY = minY;
      this.maxX = maxX;
      this.maxY = maxY;
      return this;
    },
    
    setBounds: function( bounds ) {
      return this.set( bounds.minX, bounds.minY, bounds.maxX, bounds.maxY );
    },
    
    // mutable union
    includeBounds: function( bounds ) {
      this.minX = Math.min( this.minX, bounds.minX );
      this.minY = Math.min( this.minY, bounds.minY );
      this.maxX = Math.max( this.maxX, bounds.maxX );
      this.maxY = Math.max( this.maxY, bounds.maxY );
      return this;
    },
    
    // mutable intersection
    constrainBounds: function( bounds ) {
      this.minX = Math.max( this.minX, bounds.minX );
      this.minY = Math.max( this.minY, bounds.minY );
      this.maxX = Math.min( this.maxX, bounds.maxX );
      this.maxY = Math.min( this.maxY, bounds.maxY );
      return this;
    },
    
    addCoordinates: function( x, y ) {
      this.minX = Math.min( this.minX, x );
      this.minY = Math.min( this.minY, y );
      this.maxX = Math.max( this.maxX, x );
      this.maxY = Math.max( this.maxY, y );
      return this;
    },
    
    addPoint: function( point ) {
      return this.addCoordinates( point.x, point.y );
    },
    
    setMinX: function( minX ) { this.minX = minX; return this; },
    setMinY: function( minY ) { this.minY = minY; return this; },
    setMaxX: function( maxX ) { this.maxX = maxX; return this; },
    setMaxY: function( maxY ) { this.maxY = maxY; return this; },
    
    // round to integral values, expanding where necessary
    roundOut: function() {
      this.minX = Math.floor( this.minX );
      this.minY = Math.floor( this.minY );
      this.maxX = Math.ceil( this.maxX );
      this.maxY = Math.ceil( this.maxY );
      return this;
    },
    
    // round to integral values, contracting where necessary
    roundIn: function() {
      this.minX = Math.ceil( this.minX );
      this.minY = Math.ceil( this.minY );
      this.maxX = Math.floor( this.maxX );
      this.maxY = Math.floor( this.maxY );
      return this;
    },
    
    // transform a bounding box.
    // NOTE that box.transformed( matrix ).transformed( inverse ) may be larger than the original box
    transform: function( matrix ) {
      // do nothing
      if ( this.isEmpty() ) {
        return this;
      }
      var minX = this.minX;
      var minY = this.minY;
      var maxX = this.maxX;
      var maxY = this.maxY;
      
      // using mutable vector so we don't create excessive instances of Vector2 during this
      // make sure all 4 corners are inside this transformed bounding box
      var vector = new dot.Vector2();
      this.setBounds( Bounds2.NOTHING );
      this.addPoint( matrix.multiplyVector2( vector.set( minX, minY ) ) );
      this.addPoint( matrix.multiplyVector2( vector.set( minX, maxY ) ) );
      this.addPoint( matrix.multiplyVector2( vector.set( maxX, minY ) ) );
      this.addPoint( matrix.multiplyVector2( vector.set( maxX, maxY ) ) );
      return this;
    },
    
    // expands on all sides by length d
    dilate: function( d ) {
      return this.set( this.minX - d, this.minY - d, this.maxX + d, this.maxY + d );
    },
    
    // contracts on all sides by length d
    erode: function( d ) {
      return this.dilate( -d );
    },
    
    shiftX: function( x ) {
      return this.setMinX( this.minX + x ).setMaxX( this.maxX + x );
    },
    
    shiftY: function( y ) {
      return this.setMinY( this.minY + y ).setMaxY( this.maxY + y );
    },
    
    shift: function( x, y ) {
      return this.shiftX( x ).shiftY( y );
    }
  };
  
  Bounds2.rect = function( x, y, width, height ) {
    return new Bounds2( x, y, x + width, y + height );
  };
  
  // specific bounds useful for operations
  Bounds2.EVERYTHING = new Bounds2( Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY );
  Bounds2.NOTHING = new Bounds2( Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY );
  
  return Bounds2;
} );

// Copyright 2002-2013, University of Colorado

/**
 * General utility functions for Scenery
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/Util',['require','SCENERY/scenery','DOT/Matrix3','DOT/Transform3','DOT/Bounds2','DOT/Vector2'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  var Matrix3 = require( 'DOT/Matrix3' );
  var Transform3 = require( 'DOT/Transform3' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Vector2 = require( 'DOT/Vector2' );
  
  // convenience function
  function p( x, y ) {
    return new Vector2( x, y );
  }
  
  // TODO: remove flag and tests after we're done
  var debugChromeBoundsScanning = false;
  
  // detect properly prefixed transform and transformOrigin properties
  var transformProperty = '';
  var transformOriginProperty = '';
  var webkitHardwareAcceleration = false;
  var mozillaHardwareAcceleration = false;
  if ( document && document.createElement ) {
    var style = document.createElement( 'div' ).style;
    
    var transformNames = [
      'transform',
      'webkitTransform',
      'oTransform',
      'mozTransform',
      'msTransform'
    ];
    var transformOriginNames = [
      'transformOrigin',
      'webkitTransformOrigin',
      'oTransformOrigin',
      'mozTransformOrigin',
      'msTransformOrigin'
    ];
    
    var i;
    
    for ( i = 0; i < transformNames.length; i++ ) {
      if ( transformNames[i] in style ) {
        transformProperty = transformNames[i];
        break;
      }
    }
    for ( i = 0; i < transformOriginNames.length; i++ ) {
      if ( transformOriginNames[i] in style ) {
        transformOriginProperty = transformOriginNames[i];
        break;
      }
    }
    if ( !transformOriginProperty ) {
      transformOriginProperty = 'transformOrigin'; // fallback, so we don't try to set an empty string property later
    }
    if ( ( 'webkitBackfaceVisibility' in style ) || ( 'webkitTransform' in style ) ) {
      webkitHardwareAcceleration = true;
    }
    if ( 'mozTransform' in style ) {
      mozillaHardwareAcceleration = true;
    }
  }
  
  scenery.Util = {
    // like _.extend, but with hardcoded support for https://github.com/documentcloud/underscore/pull/986
    extend: function( obj ) {
      _.each( Array.prototype.slice.call( arguments, 1 ), function( source ) {
        if ( source ) {
          for ( var prop in source ) {
            Object.defineProperty( obj, prop, Object.getOwnPropertyDescriptor( source, prop ) );
          }
        }
      });
      return obj;
    },
    
    // Object.create polyfill
    objectCreate: Object.create || function ( o ) {
      if ( arguments.length > 1 ) {
        throw new Error( 'Object.create implementation only accepts the first parameter.' );
      }
      function F() {}

      F.prototype = o;
      return new F();
    },
    
    applyCSSTransform: function( matrix, element ) {
      var transformCSS = matrix.getCSSTransform();
      // notes on triggering hardware acceleration: http://creativejs.com/2011/12/day-2-gpu-accelerate-your-dom-elements/
      // TODO: consider leaving out on iOS if possible, since we might be overflowing the GPU memory
      
      // TODO: we may want control over this flag
      if ( webkitHardwareAcceleration || mozillaHardwareAcceleration ) {
        transformCSS += ' translateZ(0)';
      }
      
      if ( webkitHardwareAcceleration ) {
        // TODO: find out bug that causes BLL text to disappear unless this acceleration is present
        element.style.webkitBackfaceVisibility = 'hidden';
      }
      
      element.style[transformProperty] = transformCSS;
      element.style[transformOriginProperty] = 'top left'; // TODO: performance: this only needs to be set once!
    },
    
    testAssert: function() {
      return 'assert.scenery: ' + ( sceneryAssert ? 'true' : 'false' );
    },
    
    testAssertExtra: function() {
      return 'assert.scenery.extra: ' + ( sceneryAssertExtra ? 'true' : 'false' );
    },
    
    /*---------------------------------------------------------------------------*
     * window.requestAnimationFrame polyfill, by Erik Moller (http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating)
     * referenced by initial Paul Irish article at http://paulirish.com/2011/requestanimationframe-for-smart-animating/
     *----------------------------------------------------------------------------*/
    polyfillRequestAnimationFrame: function() {
      var lastTime = 0;
      var vendors = [ 'ms', 'moz', 'webkit', 'o' ];
      for ( var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x ) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
      }
     
      if ( !window.requestAnimationFrame ) {
        window.requestAnimationFrame = function(callback) {
          var currTime = new Date().getTime();
          var timeToCall = Math.max(0, 16 - (currTime - lastTime));
          var id = window.setTimeout(function() { callback(currTime + timeToCall); },
            timeToCall);
          lastTime = currTime + timeToCall;
          return id;
        };
      }
     
      if ( !window.cancelAnimationFrame ) {
        window.cancelAnimationFrame = function(id) {
          clearTimeout(id);
        };
      }
    },
    
    backingStorePixelRatio: function( context ) {
      return context.webkitBackingStorePixelRatio ||
             context.mozBackingStorePixelRatio ||
             context.msBackingStorePixelRatio ||
             context.oBackingStorePixelRatio ||
             context.backingStorePixelRatio || 1;
    },
    
    // see http://developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html#//apple_ref/doc/uid/TP40010542-CH2-SW5
    // and updated based on http://www.html5rocks.com/en/tutorials/canvas/hidpi/
    backingScale: function ( context ) {
      if ( 'devicePixelRatio' in window ) {
        var backingStoreRatio = Util.backingStorePixelRatio( context );
        
        return window.devicePixelRatio / backingStoreRatio;
      }
      return 1;
    },
    
    // given a data snapshot and transform, calculate range on how large / small the bounds can be
    // very conservative, with an effective 1px extra range to allow for differences in anti-aliasing
    // for performance concerns, this does not support skews / rotations / anything but translation and scaling
    scanBounds: function( imageData, resolution, transform ) {
      
      // entry will be true if any pixel with the given x or y value is non-rgba(0,0,0,0)
      var dirtyX = _.map( _.range( resolution ), function() { return false; } );
      var dirtyY = _.map( _.range( resolution ), function() { return false; } );
      
      for ( var x = 0; x < resolution; x++ ) {
        for ( var y = 0; y < resolution; y++ ) {
          var offset = 4 * ( y * resolution + x );
          if ( imageData.data[offset] !== 0 || imageData.data[offset+1] !== 0 || imageData.data[offset+2] !== 0 || imageData.data[offset+3] !== 0 ) {
            dirtyX[x] = true;
            dirtyY[y] = true;
          }
        }
      }
      
      var minX = _.indexOf( dirtyX, true );
      var maxX = _.lastIndexOf( dirtyX, true );
      var minY = _.indexOf( dirtyY, true );
      var maxY = _.lastIndexOf( dirtyY, true );
      
      // based on pixel boundaries. for minBounds, the inner edge of the dirty pixel. for maxBounds, the outer edge of the adjacent non-dirty pixel
      // results in a spread of 2 for the identity transform (or any translated form)
      var extraSpread = resolution / 16; // is Chrome antialiasing really like this? dear god... TODO!!!
      return {
        minBounds: new Bounds2(
          ( minX < 1 || minX >= resolution - 1 ) ? Number.POSITIVE_INFINITY : transform.inversePosition2( p( minX + 1 + extraSpread, 0 ) ).x,
          ( minY < 1 || minY >= resolution - 1 ) ? Number.POSITIVE_INFINITY : transform.inversePosition2( p( 0, minY + 1 + extraSpread ) ).y,
          ( maxX < 1 || maxX >= resolution - 1 ) ? Number.NEGATIVE_INFINITY : transform.inversePosition2( p( maxX - extraSpread, 0 ) ).x,
          ( maxY < 1 || maxY >= resolution - 1 ) ? Number.NEGATIVE_INFINITY : transform.inversePosition2( p( 0, maxY - extraSpread ) ).y
        ),
        maxBounds: new Bounds2(
          ( minX < 1 || minX >= resolution - 1 ) ? Number.NEGATIVE_INFINITY : transform.inversePosition2( p( minX - 1 - extraSpread, 0 ) ).x,
          ( minY < 1 || minY >= resolution - 1 ) ? Number.NEGATIVE_INFINITY : transform.inversePosition2( p( 0, minY - 1 - extraSpread ) ).y,
          ( maxX < 1 || maxX >= resolution - 1 ) ? Number.POSITIVE_INFINITY : transform.inversePosition2( p( maxX + 2 + extraSpread, 0 ) ).x,
          ( maxY < 1 || maxY >= resolution - 1 ) ? Number.POSITIVE_INFINITY : transform.inversePosition2( p( 0, maxY + 2 + extraSpread ) ).y
        )
      };
    },
    
    canvasAccurateBounds: function( renderToContext, options ) {
      // how close to the actual bounds do we need to be?
      var precision = ( options && options.precision ) ? options.precision : 0.001;
      
      // 512x512 default square resolution
      var resolution = ( options && options.resolution ) ? options.resolution : 128;
      
      // at 1/16x default, we want to be able to get the bounds accurately for something as large as 16x our initial resolution
      // divisible by 2 so hopefully we avoid more quirks from Canvas rendering engines
      var initialScale = ( options && options.initialScale ) ? options.initialScale : (1/16);
      
      var minBounds = Bounds2.NOTHING;
      var maxBounds = Bounds2.EVERYTHING;
      
      var canvas = document.createElement( 'canvas' );
      canvas.width = resolution;
      canvas.height = resolution;
      var context = canvas.getContext( '2d' );
      
      if ( debugChromeBoundsScanning ) {
        $( window ).ready( function() {
          var header = document.createElement( 'h2' );
          $( header ).text( 'Bounds Scan' );
          $( '#display' ).append( header );
        } );
      }
      
      function scan( transform ) {
        // save/restore, in case the render tries to do any funny stuff like clipping, etc.
        context.save();
        transform.matrix.canvasSetTransform( context );
        renderToContext( context );
        context.restore();
        
        var data = context.getImageData( 0, 0, resolution, resolution );
        var minMaxBounds = Util.scanBounds( data, resolution, transform );
        
        function snapshotToCanvas( snapshot ) {
            var canvas = document.createElement( 'canvas' );
            canvas.width = resolution;
            canvas.height = resolution;
            var context = canvas.getContext( '2d' );
            context.putImageData( snapshot, 0, 0 );
            $( canvas ).css( 'border', '1px solid black' );
            $( window ).ready( function() {
              //$( '#display' ).append( $( document.createElement( 'div' ) ).text( 'Bounds: ' +  ) );
              $( '#display' ).append( canvas );
            } );
          }
        
        // TODO: remove after debug
        if ( debugChromeBoundsScanning ) {
          snapshotToCanvas( data );
        }
        
        context.clearRect( 0, 0, resolution, resolution );
        
        return minMaxBounds;
      }
      
      // attempts to map the bounds specified to the entire testing canvas (minus a fine border), so we can nail down the location quickly
      function idealTransform( bounds ) {
        // so that the bounds-edge doesn't land squarely on the boundary
        var borderSize = 2;
        
        var scaleX = ( resolution - borderSize * 2 ) / ( bounds.maxX - bounds.minX );
        var scaleY = ( resolution - borderSize * 2 ) / ( bounds.maxY - bounds.minY );
        var translationX = -scaleX * bounds.minX + borderSize;
        var translationY = -scaleY * bounds.minY + borderSize;
        
        return new Transform3( Matrix3.translation( translationX, translationY ).timesMatrix( Matrix3.scaling( scaleX, scaleY ) ) );
      }
      
      var initialTransform = new Transform3( );
      // make sure to initially center our object, so we don't miss the bounds
      initialTransform.append( Matrix3.translation( resolution / 2, resolution / 2 ) );
      initialTransform.append( Matrix3.scaling( initialScale ) );
      
      var coarseBounds = scan( initialTransform );
      
      minBounds = minBounds.union( coarseBounds.minBounds );
      maxBounds = maxBounds.intersection( coarseBounds.maxBounds );
      
      var tempMin, tempMax, refinedBounds;
      
      // minX
      tempMin = maxBounds.minY;
      tempMax = maxBounds.maxY;
      while ( isFinite( minBounds.minX ) && isFinite( maxBounds.minX ) && Math.abs( minBounds.minX - maxBounds.minX ) > precision ) {
        // use maximum bounds except for the x direction, so we don't miss things that we are looking for
        refinedBounds = scan( idealTransform( new Bounds2( maxBounds.minX, tempMin, minBounds.minX, tempMax ) ) );
        
        if ( minBounds.minX <= refinedBounds.minBounds.minX && maxBounds.minX >= refinedBounds.maxBounds.minX ) {
          // sanity check - break out of an infinite loop!
          if ( debugChromeBoundsScanning ) {
            console.log( 'warning, exiting infinite loop!' );
            console.log( 'transformed "min" minX: ' + idealTransform( new Bounds2( maxBounds.minX, maxBounds.minY, minBounds.minX, maxBounds.maxY ) ).transformPosition2( p( minBounds.minX, 0 ) ) );
            console.log( 'transformed "max" minX: ' + idealTransform( new Bounds2( maxBounds.minX, maxBounds.minY, minBounds.minX, maxBounds.maxY ) ).transformPosition2( p( maxBounds.minX, 0 ) ) );
          }
          break;
        }
        
        minBounds = minBounds.withMinX( Math.min( minBounds.minX, refinedBounds.minBounds.minX ) );
        maxBounds = maxBounds.withMinX( Math.max( maxBounds.minX, refinedBounds.maxBounds.minX ) );
        tempMin = Math.max( tempMin, refinedBounds.maxBounds.minY );
        tempMax = Math.min( tempMax, refinedBounds.maxBounds.maxY );
      }
      
      // maxX
      tempMin = maxBounds.minY;
      tempMax = maxBounds.maxY;
      while ( isFinite( minBounds.maxX ) && isFinite( maxBounds.maxX ) && Math.abs( minBounds.maxX - maxBounds.maxX ) > precision ) {
        // use maximum bounds except for the x direction, so we don't miss things that we are looking for
        refinedBounds = scan( idealTransform( new Bounds2( minBounds.maxX, tempMin, maxBounds.maxX, tempMax ) ) );
        
        if ( minBounds.maxX >= refinedBounds.minBounds.maxX && maxBounds.maxX <= refinedBounds.maxBounds.maxX ) {
          // sanity check - break out of an infinite loop!
          if ( debugChromeBoundsScanning ) {
            console.log( 'warning, exiting infinite loop!' );
          }
          break;
        }
        
        minBounds = minBounds.withMaxX( Math.max( minBounds.maxX, refinedBounds.minBounds.maxX ) );
        maxBounds = maxBounds.withMaxX( Math.min( maxBounds.maxX, refinedBounds.maxBounds.maxX ) );
        tempMin = Math.max( tempMin, refinedBounds.maxBounds.minY );
        tempMax = Math.min( tempMax, refinedBounds.maxBounds.maxY );
      }
      
      // minY
      tempMin = maxBounds.minX;
      tempMax = maxBounds.maxX;
      while ( isFinite( minBounds.minY ) && isFinite( maxBounds.minY ) && Math.abs( minBounds.minY - maxBounds.minY ) > precision ) {
        // use maximum bounds except for the y direction, so we don't miss things that we are looking for
        refinedBounds = scan( idealTransform( new Bounds2( tempMin, maxBounds.minY, tempMax, minBounds.minY ) ) );
        
        if ( minBounds.minY <= refinedBounds.minBounds.minY && maxBounds.minY >= refinedBounds.maxBounds.minY ) {
          // sanity check - break out of an infinite loop!
          if ( debugChromeBoundsScanning ) {
            console.log( 'warning, exiting infinite loop!' );
          }
          break;
        }
        
        minBounds = minBounds.withMinY( Math.min( minBounds.minY, refinedBounds.minBounds.minY ) );
        maxBounds = maxBounds.withMinY( Math.max( maxBounds.minY, refinedBounds.maxBounds.minY ) );
        tempMin = Math.max( tempMin, refinedBounds.maxBounds.minX );
        tempMax = Math.min( tempMax, refinedBounds.maxBounds.maxX );
      }
      
      // maxY
      tempMin = maxBounds.minX;
      tempMax = maxBounds.maxX;
      while ( isFinite( minBounds.maxY ) && isFinite( maxBounds.maxY ) && Math.abs( minBounds.maxY - maxBounds.maxY ) > precision ) {
        // use maximum bounds except for the y direction, so we don't miss things that we are looking for
        refinedBounds = scan( idealTransform( new Bounds2( tempMin, minBounds.maxY, tempMax, maxBounds.maxY ) ) );
        
        if ( minBounds.maxY >= refinedBounds.minBounds.maxY && maxBounds.maxY <= refinedBounds.maxBounds.maxY ) {
          // sanity check - break out of an infinite loop!
          if ( debugChromeBoundsScanning ) {
            console.log( 'warning, exiting infinite loop!' );
          }
          break;
        }
        
        minBounds = minBounds.withMaxY( Math.max( minBounds.maxY, refinedBounds.minBounds.maxY ) );
        maxBounds = maxBounds.withMaxY( Math.min( maxBounds.maxY, refinedBounds.maxBounds.maxY ) );
        tempMin = Math.max( tempMin, refinedBounds.maxBounds.minX );
        tempMax = Math.min( tempMax, refinedBounds.maxBounds.maxX );
      }
      
      if ( debugChromeBoundsScanning ) {
        console.log( 'minBounds: ' + minBounds );
        console.log( 'maxBounds: ' + maxBounds );
      }
      
      var result = new Bounds2(
        ( minBounds.minX + maxBounds.minX ) / 2,
        ( minBounds.minY + maxBounds.minY ) / 2,
        ( minBounds.maxX + maxBounds.maxX ) / 2,
        ( minBounds.maxY + maxBounds.maxY ) / 2
      );
      
      // extra data about our bounds
      result.minBounds = minBounds;
      result.maxBounds = maxBounds;
      result.isConsistent = maxBounds.containsBounds( minBounds );
      result.precision = Math.max(
        Math.abs( minBounds.minX - maxBounds.minX ),
        Math.abs( minBounds.minY - maxBounds.minY ),
        Math.abs( minBounds.maxX - maxBounds.maxX ),
        Math.abs( minBounds.maxY - maxBounds.maxY )
      );
      
      // return the average
      return result;
    }
  };
  var Util = scenery.Util;
  
  return Util;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * The main 'kite' namespace object for the exported (non-Require.js) API. Used internally
 * since it prevents Require.js issues with circular dependencies.
 *
 * The returned kite object namespace may be incomplete if not all modules are listed as
 * dependencies. Please use the 'main' module for that purpose if all of Kite is desired.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/kite',['require','ASSERT/assert','PHET_CORE/phetAllocation'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );
  
  // object allocation tracking
  window.phetAllocation = require( 'PHET_CORE/phetAllocation' );
  
  var kite = {};
  
  kite.assert = assert;
  
  // will be filled in by other modules
  return kite;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * A segment represents a specific curve with a start and end.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/segments/Segment',['require','ASSERT/assert','KITE/kite','DOT/Util'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );

  var kite = require( 'KITE/kite' );
  
  var DotUtil = require( 'DOT/Util' );
  
  /*
   * Will contain (for segments):
   * properties:
   * start        - start point of this segment
   * end          - end point of this segment
   * startTangent - the tangent vector (normalized) to the segment at the start, pointing in the direction of motion (from start to end)
   * endTangent   - the tangent vector (normalized) to the segment at the end, pointing in the direction of motion (from start to end)
   * bounds       - the bounding box for the segment
   *
   * methods:
   * positionAt( t )          - returns the position parametrically, with 0 <= t <= 1. this does NOT guarantee a constant magnitude tangent... don't feel like adding elliptical functions yet!
   * tangentAt( t )           - returns the non-normalized tangent (dx/dt, dy/dt) parametrically, with 0 <= t <= 1.
   * curvatureAt( t )         - returns the signed curvature (positive for visual clockwise - mathematical counterclockwise)
   * subdivided( t, skip )    - returns an array with 2 sub-segments, split at the parametric t value. if skip is passed, expensive operations are not performed
   * getSVGPathFragment()     - returns a string containing the SVG path. assumes that the start point is already provided, so anything that calls this needs to put the M calls first
   * strokeLeft( lineWidth )  - returns an array of segments that will draw an offset curve on the logical left side
   * strokeRight( lineWidth ) - returns an array of segments that will draw an offset curve on the logical right side
   * intersectsBounds         - whether this segment intersects the specified bounding box (not just the segment's bounding box, but the actual segment)
   * windingIntersection      - returns the winding number for intersection with a ray
   *
   * writeToContext( context ) - draws the segment to the 2D Canvas context, assuming the context's current location is already at the start point
   * transformed( matrix )     - returns a new segment that represents this segment after transformation by the matrix
   */
  kite.Segment = function Segment(){}; // no common construction for now
  var Segment = kite.Segment;
  
  Segment.prototype = {
    constructor: Segment,
    
    // tList should be a list of sorted t values from 0 <= t <= 1
    subdivisions: function( tList, skipComputation ) {
      // this could be solved by recursion, but we don't plan on the JS engine doing tail-call optimization
      var right = this;
      var result = [];
      for ( var i = 0; i < tList.length; i++ ) {
        // assume binary subdivision
        var t = tList[i];
        var arr = right.subdivided( t, skipComputation );
        assert && assert( arr.length === 2 );
        result.push( arr[0] );
        right = arr[1];
        
        // scale up the remaining t values
        for ( var j = i + 1; j < tList.length; j++ ) {
          tList[j] = DotUtil.linear( t, 1, 0, 1, tList[j] );
        }
      }
      result.push( right );
      return result;
    },
    
    // return an array of segments from breaking this segment into monotone pieces
    subdividedIntoMonotone: function() {
      return this.subdivisions( this.getInteriorExtremaTs() );
    },
  };
  
  return Segment;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Linear segment
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/segments/Line',['require','ASSERT/assert','KITE/kite','PHET_CORE/inherit','DOT/Bounds2','DOT/Util','KITE/segments/Segment'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );

  var kite = require( 'KITE/kite' );
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var lineLineIntersection = require( 'DOT/Util' ).lineLineIntersection;
  
  var Segment = require( 'KITE/segments/Segment' );

  Segment.Line = function Line( start, end ) {
    this.start = start;
    this.end = end;
    
    if ( start.equals( end, 0 ) ) {
      this.invalid = true;
      return;
    }
    
    this.startTangent = end.minus( start ).normalized();
    this.endTangent = this.startTangent;
    
    // acceleration for intersection
    this.bounds = Bounds2.NOTHING.withPoint( start ).withPoint( end );
  };
  inherit( Segment, Segment.Line, {
    
    positionAt: function( t ) {
      return this.start.plus( this.end.minus( this.start ).times( t ) );
    },
    
    tangentAt: function( t ) {
      // tangent always the same, just use the start tanget
      return this.startTangent;
    },
    
    curvatureAt: function( t ) {
      return 0; // no curvature on a straight line segment
    },
    
    getSVGPathFragment: function() {
      return 'L ' + this.end.x + ' ' + this.end.y;
    },
    
    strokeLeft: function( lineWidth ) {
      var offset = this.endTangent.perpendicular().negated().times( lineWidth / 2 );
      return [new Segment.Line( this.start.plus( offset ), this.end.plus( offset ) )];
    },
    
    strokeRight: function( lineWidth ) {
      var offset = this.startTangent.perpendicular().times( lineWidth / 2 );
      return [new Segment.Line( this.end.plus( offset ), this.start.plus( offset ) )];
    },
    
    // lines are already monotone
    getInteriorExtremaTs: function() { return []; },
    
    subdivided: function( t ) {
      var pt = this.positionAt( t );
      return [
        new Segment.Line( this.start, pt ),
        new Segment.Line( pt, this.end )
      ];
    },
    
    intersectsBounds: function( bounds ) {
      throw new Error( 'Segment.Line.intersectsBounds unimplemented' ); // TODO: implement
    },
    
    intersection: function( ray ) {
      var result = [];
      
      var start = this.start;
      var end = this.end;
      
      var intersection = lineLineIntersection( start, end, ray.pos, ray.pos.plus( ray.dir ) );
      
      if ( !isFinite( intersection.x ) || !isFinite( intersection.y ) ) {
        // lines must be parallel
        return result;
      }
      
      // check to make sure our point is in our line segment (specifically, in the bounds (start,end], not including the start point so we don't double-count intersections)
      if ( start.x !== end.x && ( start.x > end.x ? ( intersection.x >= start.x || intersection.x < end.x ) : ( intersection.x <= start.x || intersection.x > end.x ) ) ) {
        return result;
      }
      if ( start.y !== end.y && ( start.y > end.y ? ( intersection.y >= start.y || intersection.y < end.y ) : ( intersection.y <= start.y || intersection.y > end.y ) ) ) {
        return result;
      }
      
      // make sure the intersection is not behind the ray
      var t = intersection.minus( ray.pos ).dot( ray.dir );
      if ( t < 0 ) {
        return result;
      }
      
      // return the proper winding direction depending on what way our line intersection is "pointed"
      var diff = end.minus( start );
      var perp = diff.perpendicular();
      result.push( {
        distance: t,
        point: ray.pointAtDistance( t ),
        normal: perp.dot( ray.dir ) > 0 ? perp.negated() : perp,
        wind: ray.dir.perpendicular().dot( diff ) < 0 ? 1 : -1
      } );
      return result;
    },
    
    // returns the resultant winding number of this ray intersecting this segment.
    windingIntersection: function( ray ) {
      var hits = this.intersection( ray );
      if ( hits.length ) {
        return hits[0].wind;
      } else {
        return 0;
      }
    },
    
    // assumes the current position is at start
    writeToContext: function( context ) {
      context.lineTo( this.end.x, this.end.y );
    },
    
    transformed: function( matrix ) {
      return new Segment.Line( matrix.timesVector2( this.start ), matrix.timesVector2( this.end ) );
    }
  } );
  
  return Segment.Line;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Arc segment
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/segments/Arc',['require','ASSERT/assert','KITE/kite','PHET_CORE/inherit','DOT/Vector2','DOT/Bounds2','DOT/Util','KITE/segments/Segment'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );

  var kite = require( 'KITE/kite' );
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Vector2 = require( 'DOT/Vector2' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var DotUtil = require( 'DOT/Util' );

  var Segment = require( 'KITE/segments/Segment' );

  Segment.Arc = function Arc( center, radius, startAngle, endAngle, anticlockwise ) {
    if ( radius < 0 ) {
      // support this case since we might actually need to handle it inside of strokes?
      radius = -radius;
      startAngle += Math.PI;
      endAngle += Math.PI;
    }
    
    this.center = center;
    this.radius = radius;
    this.startAngle = startAngle;
    this.endAngle = endAngle;
    this.anticlockwise = anticlockwise;
    
    this.start = this.positionAtAngle( startAngle );
    this.end = this.positionAtAngle( endAngle );
    this.startTangent = this.tangentAtAngle( startAngle );
    this.endTangent = this.tangentAtAngle( endAngle );
    
    if ( radius <= 0 || startAngle === endAngle ) {
      this.invalid = true;
      return;
    }
    
    // compute an actual end angle so that we can smoothly go from this.startAngle to this.actualEndAngle
    if ( this.anticlockwise ) {
      // angle is 'decreasing'
      // -2pi <= end - start < 2pi
      if ( this.startAngle > this.endAngle ) {
        this.actualEndAngle = this.endAngle;
      } else if ( this.startAngle < this.endAngle ) {
        this.actualEndAngle = this.endAngle - 2 * Math.PI;
      } else {
        // equal
        this.actualEndAngle = this.startAngle;
      }
    } else {
      // angle is 'increasing'
      // -2pi < end - start <= 2pi
      if ( this.startAngle < this.endAngle ) {
        this.actualEndAngle = this.endAngle;
      } else if ( this.startAngle > this.endAngle ) {
        this.actualEndAngle = this.endAngle + Math.PI * 2;
      } else {
        // equal
        this.actualEndAngle = this.startAngle;
      }
    }
    
    // constraints
    assert && assert( !( ( !anticlockwise && endAngle - startAngle <= -Math.PI * 2 ) || ( anticlockwise && startAngle - endAngle <= -Math.PI * 2 ) ), 'Not handling arcs with start/end angles that show differences in-between browser handling' );
    assert && assert( !( ( !anticlockwise && endAngle - startAngle > Math.PI * 2 ) || ( anticlockwise && startAngle - endAngle > Math.PI * 2 ) ), 'Not handling arcs with start/end angles that show differences in-between browser handling' );
    
    var isFullPerimeter = ( !anticlockwise && endAngle - startAngle >= Math.PI * 2 ) || ( anticlockwise && startAngle - endAngle >= Math.PI * 2 );
    
    // compute an angle difference that represents how "much" of the circle our arc covers
    this.angleDifference = this.anticlockwise ? this.startAngle - this.endAngle : this.endAngle - this.startAngle;
    if ( this.angleDifference < 0 ) {
      this.angleDifference += Math.PI * 2;
    }
    assert && assert( this.angleDifference >= 0 ); // now it should always be zero or positive
    
    // acceleration for intersection
    this.bounds = Bounds2.NOTHING;
    this.bounds = this.bounds.withPoint( this.start );
    this.bounds = this.bounds.withPoint( this.end );
    
    // for bounds computations
    var that = this;
    function boundsAtAngle( angle ) {
      if ( that.containsAngle( angle ) ) {
        // the boundary point is in the arc
        that.bounds = that.bounds.withPoint( center.plus( Vector2.createPolar( radius, angle ) ) );
      }
    }
    
    // if the angles are different, check extrema points
    if ( startAngle !== endAngle ) {
      // check all of the extrema points
      boundsAtAngle( 0 );
      boundsAtAngle( Math.PI / 2 );
      boundsAtAngle( Math.PI );
      boundsAtAngle( 3 * Math.PI / 2 );
    }
  };
  inherit( Segment, Segment.Arc, {
    
    // maps a contained angle to between [startAngle,actualEndAngle), even if the end angle is lower.
    mapAngle: function( angle ) {
      // consider an assert that we contain that angle?
      return ( this.startAngle > this.actualEndAngle ) ?
             DotUtil.moduloBetweenUp( angle, this.startAngle - 2 * Math.PI, this.startAngle ) :
             DotUtil.moduloBetweenDown( angle, this.startAngle, this.startAngle + 2 * Math.PI );
    },
    
    tAtAngle: function( angle ) {
      return ( this.mapAngle( angle ) - this.startAngle ) / ( this.actualEndAngle - this.startAngle );
    },
    
    angleAt: function( t ) {
      return this.startAngle + ( this.actualEndAngle - this.startAngle ) * t;
    },
    
    positionAt: function( t ) {
      return this.positionAtAngle( this.angleAt( t ) );
    },
    
    tangentAt: function( t ) {
      return this.tangentAtAngle( this.angleAt( t ) );
    },
    
    curvatureAt: function( t ) {
      return ( this.anticlockwise ? -1 : 1 ) / this.radius;
    },
    
    positionAtAngle: function( angle ) {
      return this.center.plus( Vector2.createPolar( this.radius, angle ) );
    },
    
    tangentAtAngle: function( angle ) {
      var normal = Vector2.createPolar( 1, angle );
      
      return this.anticlockwise ? normal.perpendicular() : normal.perpendicular().negated();
    },
    
    // TODO: refactor? shared with Segment.EllipticalArc (use this improved version)
    containsAngle: function( angle ) {
      // transform the angle into the appropriate coordinate form
      // TODO: check anticlockwise version!
      var normalizedAngle = this.anticlockwise ? angle - this.endAngle : angle - this.startAngle;
      
      // get the angle between 0 and 2pi
      var positiveMinAngle = DotUtil.moduloBetweenDown( normalizedAngle, 0, Math.PI * 2 );
      
      return positiveMinAngle <= this.angleDifference;
    },
    
    getSVGPathFragment: function() {
      // see http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands for more info
      // rx ry x-axis-rotation large-arc-flag sweep-flag x y
      
      var epsilon = 0.01; // allow some leeway to render things as 'almost circles'
      var sweepFlag = this.anticlockwise ? '0' : '1';
      var largeArcFlag;
      if ( this.angleDifference < Math.PI * 2 - epsilon ) {
        largeArcFlag = this.angleDifference < Math.PI ? '0' : '1';
        return 'A ' + this.radius + ' ' + this.radius + ' 0 ' + largeArcFlag + ' ' + sweepFlag + ' ' + this.end.x + ' ' + this.end.y;
      } else {
        // circle (or almost-circle) case needs to be handled differently
        // since SVG will not be able to draw (or know how to draw) the correct circle if we just have a start and end, we need to split it into two circular arcs
        
        // get the angle that is between and opposite of both of the points
        var splitOppositeAngle = ( this.startAngle + this.endAngle ) / 2; // this _should_ work for the modular case?
        var splitPoint = this.center.plus( Vector2.createPolar( this.radius, splitOppositeAngle ) );
        
        largeArcFlag = '0'; // since we split it in 2, it's always the small arc
        
        var firstArc = 'A ' + this.radius + ' ' + this.radius + ' 0 ' + largeArcFlag + ' ' + sweepFlag + ' ' + splitPoint.x + ' ' + splitPoint.y;
        var secondArc = 'A ' + this.radius + ' ' + this.radius + ' 0 ' + largeArcFlag + ' ' + sweepFlag + ' ' + this.end.x + ' ' + this.end.y;
        
        return firstArc + ' ' + secondArc;
      }
    },
    
    strokeLeft: function( lineWidth ) {
      return [new Segment.Arc( this.center, this.radius + ( this.anticlockwise ? 1 : -1 ) * lineWidth / 2, this.startAngle, this.endAngle, this.anticlockwise )];
    },
    
    strokeRight: function( lineWidth ) {
      return [new Segment.Arc( this.center, this.radius + ( this.anticlockwise ? -1 : 1 ) * lineWidth / 2, this.endAngle, this.startAngle, !this.anticlockwise )];
    },
    
    // not including 0 and 1
    getInteriorExtremaTs: function() {
      var that = this;
      var result = [];
      _.each( [ 0, Math.PI / 2, Math.PI, 3 * Math.PI / 2 ], function( angle ) {
        if ( that.containsAngle( angle ) ) {
          var t = that.tAtAngle( angle );
          var epsilon = 0.0000000001; // TODO: general kite epsilon?
          if ( t > epsilon && t < 1 - epsilon ) {
            result.push( t );
          }
        }
      } );
      return result.sort(); // modifies original, which is OK
    },
    
    subdivided: function( t ) {
      // TODO: verify that we don't need to switch anticlockwise here, or subtract 2pi off any angles
      var angle0 = this.angleAt( 0 );
      var angleT = this.angleAt( t );
      var angle1 = this.angleAt( 1 );
      return [
        new Segment.Arc( this.center, this.radius, angle0, angleT, this.anticlockwise ),
        new Segment.Arc( this.center, this.radius, angleT, angle1, this.anticlockwise )
      ];
    },
    
    intersectsBounds: function( bounds ) {
      throw new Error( 'Segment.intersectsBounds unimplemented!' );
    },
    
    intersection: function( ray ) {
      var result = []; // hits in order
      
      // left here, if in the future we want to better-handle boundary points
      var epsilon = 0;
      
      // Run a general circle-intersection routine, then we can test the angles later.
      // Solves for the two solutions t such that ray.pos + ray.dir * t is on the circle.
      // Then we check whether the angle at each possible hit point is in our arc.
      var centerToRay = ray.pos.minus( this.center );
      var tmp = ray.dir.dot( centerToRay );
      var centerToRayDistSq = centerToRay.magnitudeSquared();
      var discriminant = 4 * tmp * tmp - 4 * ( centerToRayDistSq - this.radius * this.radius );
      if ( discriminant < epsilon ) {
        // ray misses circle entirely
        return result;
      }
      var base = ray.dir.dot( this.center ) - ray.dir.dot( ray.pos );
      var sqt = Math.sqrt( discriminant ) / 2;
      var ta = base - sqt;
      var tb = base + sqt;
      
      if ( tb < epsilon ) {
        // circle is behind ray
        return result;
      }
      
      var pointB = ray.pointAtDistance( tb );
      var normalB = pointB.minus( this.center ).normalized();
      
      if ( ta < epsilon ) {
        // we are inside the circle, so only one intersection is possible
        if ( this.containsAngle( normalB.angle() ) ) {
          result.push( {
            distance: tb,
            point: pointB,
            normal: normalB.negated(), // normal is towards the ray
            wind: this.anticlockwise ? -1 : 1 // since we are inside, wind this way
          } );
        }
      }
      else {
        // two possible hits (outside circle)
        var pointA = ray.pointAtDistance( ta );
        var normalA = pointA.minus( this.center ).normalized();
        
        if ( this.containsAngle( normalA.angle() ) ) {
          result.push( {
            distance: ta,
            point: pointA,
            normal: normalA,
            wind: this.anticlockwise ? 1 : -1 // hit from outside
          } );
        }
        if ( this.containsAngle( normalB.angle() ) ) {
          result.push( {
            distance: tb,
            point: pointB,
            normal: normalB.negated(),
            wind: this.anticlockwise ? -1 : 1 // this is the far hit, which winds the opposite way
          } );
        }
      }
      
      return result;
    },
    
    // returns the resultant winding number of this ray intersecting this segment.
    windingIntersection: function( ray ) {
      var wind = 0;
      var hits = this.intersection( ray );
      _.each( hits, function( hit ) {
        wind += hit.wind;
      } );
      return wind;
    },
    
    writeToContext: function( context ) {
      context.arc( this.center.x, this.center.y, this.radius, this.startAngle, this.endAngle, this.anticlockwise );
    },
    
    // TODO: test various transform types, especially rotations, scaling, shears, etc.
    transformed: function( matrix ) {
      // so we can handle reflections in the transform, we do the general case handling for start/end angles
      var startAngle = matrix.timesVector2( Vector2.createPolar( 1, this.startAngle ) ).minus( matrix.timesVector2( Vector2.ZERO ) ).angle();
      var endAngle = matrix.timesVector2( Vector2.createPolar( 1, this.endAngle ) ).minus( matrix.timesVector2( Vector2.ZERO ) ).angle();
      
      // reverse the 'clockwiseness' if our transform includes a reflection
      var anticlockwise = matrix.getDeterminant() >= 0 ? this.anticlockwise : !this.anticlockwise;

      var scaleVector = matrix.getScaleVector();
      if ( scaleVector.x !== scaleVector.y ) {
        var radiusX = scaleVector.x * this.radius;
        var radiusY = scaleVector.y * this.radius;
        return new Segment.EllipticalArc( matrix.timesVector2( this.center ), radiusX, radiusY, 0, startAngle, endAngle, anticlockwise );
      } else {
        var radius = scaleVector.x * this.radius;
        return new Segment.Arc( matrix.timesVector2( this.center ), radius, startAngle, endAngle, anticlockwise );
      }
    }
  } );
  
  return Segment.Arc;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * A Canvas-style stateful (mutable) subpath, which tracks segments in addition to the points.
 *
 * See http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#concept-path
 * for the path / subpath Canvas concept.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/util/Subpath',['require','ASSERT/assert','DOT/Vector2','DOT/Bounds2','DOT/Util','KITE/kite','KITE/segments/Line','KITE/segments/Arc'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );
  
  var Vector2 = require( 'DOT/Vector2' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var lineLineIntersection = require( 'DOT/Util' ).lineLineIntersection;
  
  var kite = require( 'KITE/kite' );
  
  require( 'KITE/segments/Line' );
  require( 'KITE/segments/Arc' );
  
  // all arguments optional (they are for the copy() method)
  kite.Subpath = function Subpath( segments, points, closed ) {
    this.segments = segments || [];
    
    // recombine points if necessary, based off of start points of segments + the end point of the last segment
    this.points = points || ( ( segments && segments.length ) ? _.map( segments, function( segment ) { return segment.start; } ).concat( segments[segments.length-1].end ) : [] );
    this.closed = !!closed;
    
    // cached stroked shape (so hit testing can be done quickly on stroked shapes)
    this._strokedSubpaths = null;
    this._strokedSubpathsComputed = false;
    this._strokedStyles = null;
  };
  var Subpath = kite.Subpath;
  Subpath.prototype = {
    copy: function() {
      return new Subpath( this.segments.slice( 0 ), this.points.slice( 0 ), this.closed );
    },
    
    invalidate: function() {
      this._strokedSubpathsComputed = false;
    },
    
    addPoint: function( point ) {
      this.points.push( point );
      
      return this; // allow chaining
    },
    
    addSegment: function( segment ) {
      if ( !segment.invalid ) {
        assert && assert( segment.start.isFinite(), 'Segment start is infinite' );
        assert && assert( segment.end.isFinite(), 'Segment end is infinite' );
        assert && assert( segment.startTangent.isFinite(), 'Segment startTangent is infinite' );
        assert && assert( segment.endTangent.isFinite(), 'Segment endTangent is infinite' );
        assert && assert( segment.bounds.isEmpty() || segment.bounds.isFinite(), 'Segment bounds is infinite and non-empty' );
        this.segments.push( segment );
        this.invalidate();
      }
      
      return this; // allow chaining
    },
    
    close: function() {
      this.closed = true;
    },
    
    getLength: function() {
      return this.points.length;
    },
    
    getFirstPoint: function() {
      return _.first( this.points );
    },
    
    getLastPoint: function() {
      return _.last( this.points );
    },
    
    getFirstSegment: function() {
      return _.first( this.segments );
    },
    
    getLastSegment: function() {
      return _.last( this.segments );
    },
    
    isDrawable: function() {
      return this.segments.length > 0;
    },
    
    isClosed: function() {
      return this.closed;
    },
    
    hasClosingSegment: function() {
      return !this.getFirstPoint().equalsEpsilon( this.getLastPoint(), 0.000000001 );
    },
    
    getClosingSegment: function() {
      assert && assert( this.hasClosingSegment(), 'Implicit closing segment unnecessary on a fully closed path' );
      return new kite.Segment.Line( this.getLastPoint(), this.getFirstPoint() );
    },
    
    writeToContext: function( context ) {
      if ( this.isDrawable() ) {
        var startPoint = this.getFirstSegment().start;
        context.moveTo( startPoint.x, startPoint.y ); // the segments assume the current context position is at their start
        
        var len = this.segments.length;
        for ( var i = 0; i < len; i++ ) {
          this.segments[i].writeToContext( context );
        }
        
        if ( this.closed ) {
          context.closePath();
        }
      }
    },
    
    transformed: function( matrix ) {
      return new Subpath(
        _.map( this.segments, function( segment ) { return segment.transformed( matrix ); } ),
        _.map( this.points, function( point ) { return matrix.timesVector2( point ); } ),
        this.closed
      );
    },
    
    computeBounds: function() {
      var bounds = Bounds2.NOTHING.copy();
      var len = this.segments.length;
      for ( var i = 0; i < len; i++ ) {
        bounds.includeBounds( this.segments[i].bounds );
      }
      return bounds;
    },
    
    // returns an array of subpaths (one if open, two if closed) that represent a stroked copy of this subpath.
    stroked: function( lineStyles ) {
      // non-drawable subpaths convert to empty subpaths
      if ( !this.isDrawable() ) {
        return [];
      }
      
      if ( lineStyles === undefined ) {
        lineStyles = new kite.LineStyles();
      }
      
      // return a cached version if possible
      if ( this._strokedSubpathsComputed && this._strokedStyles.equals( lineStyles ) ) {
        return this._strokedSubpaths;
      }
      
      var lineWidth = lineStyles.lineWidth;
      
      // joins two segments together on the logical "left" side, at 'center' (where they meet), and normalized tangent vectors in the direction of the stroking
      // to join on the "right" side, switch the tangent order and negate them
      function join( center, fromTangent, toTangent ) {
        // where our join path starts and ends
        var fromPoint = center.plus( fromTangent.perpendicular().negated().times( lineWidth / 2 ) );
        var toPoint = center.plus( toTangent.perpendicular().negated().times( lineWidth / 2 ) );
        
        var bevel = ( fromPoint.equals( toPoint ) ? [] : [new kite.Segment.Line( fromPoint, toPoint )] );
        
        // only insert a join on the non-acute-angle side
        if ( fromTangent.perpendicular().dot( toTangent ) > 0 ) {
          switch( lineStyles.lineJoin ) {
            case 'round':
              var fromAngle = fromTangent.angle() + Math.PI / 2;
              var toAngle = toTangent.angle() + Math.PI / 2;
              return [new kite.Segment.Arc( center, lineWidth / 2, fromAngle, toAngle, true )];
            case 'miter':
              var theta = fromTangent.angleBetween( toTangent.negated() );
              var notStraight = theta < Math.PI - 0.00001; // if fromTangent is approximately equal to toTangent, just bevel. it will be indistinguishable
              if ( 1 / Math.sin( theta / 2 ) <= lineStyles.miterLimit && theta < Math.PI - 0.00001 ) {
                // draw the miter
                var miterPoint = lineLineIntersection( fromPoint, fromPoint.plus( fromTangent ), toPoint, toPoint.plus( toTangent ) );
                return [
                  new kite.Segment.Line( fromPoint, miterPoint ),
                  new kite.Segment.Line( miterPoint, toPoint )
                ];
              } else {
                // angle too steep, use bevel instead. same as below, but copied for linter
                return bevel;
              }
              break;
            case 'bevel':
              return bevel;
          }
        } else {
          // no join necessary here since we have the acute angle. just simple lineTo for now so that the next segment starts from the right place
          // TODO: can we prevent self-intersection here?
          return bevel;
        }
      }
      
      // draws the necessary line cap from the endpoint 'center' in the direction of the tangent
      function cap( center, tangent ) {
        var fromPoint = center.plus( tangent.perpendicular().times( -lineWidth / 2 ) );
        var toPoint = center.plus( tangent.perpendicular().times( lineWidth / 2 ) );
        
        switch( lineStyles.lineCap ) {
          case 'butt':
            return [new kite.Segment.Line( fromPoint, toPoint )];
          case 'round':
            var tangentAngle = tangent.angle();
            return [new kite.Segment.Arc( center, lineWidth / 2, tangentAngle + Math.PI / 2, tangentAngle - Math.PI / 2, true )];
          case 'square':
            var toLeft = tangent.perpendicular().negated().times( lineWidth / 2 );
            var toRight = tangent.perpendicular().times( lineWidth / 2 );
            var toFront = tangent.times( lineWidth / 2 );
            
            var left = center.plus( toLeft ).plus( toFront );
            var right = center.plus( toRight ).plus( toFront );
            return [
              new kite.Segment.Line( fromPoint, left ),
              new kite.Segment.Line( left, right ),
              new kite.Segment.Line( right, toPoint )
            ];
        }
      }
      
      var i;
      var leftSegments = [];
      var rightSegments = [];
      var firstSegment = this.getFirstSegment();
      var lastSegment = this.getLastSegment();
      
      function addLeftSegments( segments ) {
        leftSegments = leftSegments.concat( segments );
      }
      function addRightSegments( segments ) {
        rightSegments = rightSegments.concat( segments );
      }
      
      // we don't need to insert an implicit closing segment if the start and end points are the same
      var alreadyClosed = lastSegment.end.equals( firstSegment.start );
      // if there is an implicit closing segment
      var closingSegment = alreadyClosed ? null : new kite.Segment.Line( this.segments[this.segments.length-1].end, this.segments[0].start );
      
      // stroke the logical "left" side of our path
      for ( i = 0; i < this.segments.length; i++ ) {
        if ( i > 0 ) {
          addLeftSegments( join( this.segments[i].start, this.segments[i-1].endTangent, this.segments[i].startTangent, true ) );
        }
        addLeftSegments( this.segments[i].strokeLeft( lineWidth ) );
      }
      
      // stroke the logical "right" side of our path
      for ( i = this.segments.length - 1; i >= 0; i-- ) {
        if ( i < this.segments.length - 1 ) {
          addRightSegments( join( this.segments[i].end, this.segments[i+1].startTangent.negated(), this.segments[i].endTangent.negated(), false ) );
        }
        addRightSegments( this.segments[i].strokeRight( lineWidth ) );
      }
      
      var subpaths;
      if ( this.closed ) {
        if ( alreadyClosed ) {
          // add the joins between the start and end
          addLeftSegments( join( lastSegment.end, lastSegment.endTangent, firstSegment.startTangent ) );
          addRightSegments( join( lastSegment.end, firstSegment.startTangent.negated(), lastSegment.endTangent.negated() ) );
        } else {
          // logical "left" stroke on the implicit closing segment
          addLeftSegments( join( closingSegment.start, lastSegment.endTangent, closingSegment.startTangent ) );
          addLeftSegments( closingSegment.strokeLeft( lineWidth ) );
          addLeftSegments( join( closingSegment.end, closingSegment.endTangent, firstSegment.startTangent ) );
          
          // logical "right" stroke on the implicit closing segment
          addRightSegments( join( closingSegment.end, firstSegment.startTangent.negated(), closingSegment.endTangent.negated() ) );
          addRightSegments( closingSegment.strokeRight( lineWidth ) );
          addRightSegments( join( closingSegment.start, closingSegment.startTangent.negated(), lastSegment.endTangent.negated() ) );
        }
        subpaths = [
          new Subpath( leftSegments, null, true ),
          new Subpath( rightSegments, null, true )
        ];
      } else {
        subpaths = [
          new Subpath( leftSegments
                         .concat( cap( lastSegment.end, lastSegment.endTangent ) )
                         .concat( rightSegments )
                         .concat( cap( firstSegment.start, firstSegment.startTangent.negated() ) ),
                       null, true )
        ];
      }
      
      this._strokedSubpaths = subpaths;
      this._strokedSubpathsComputed = true;
      this._strokedStyles = new kite.LineStyles( lineStyles ); // shallow copy, since we consider linestyles to be mutable
      
      return subpaths;
    }
  };
  
  // TODO: performance / cleanliness to have these as methods instead?
  function segmentStartLeft( segment, lineWidth ) {
    assert && assert( lineWidth !== undefined );
    return segment.start.plus( segment.startTangent.perpendicular().negated().times( lineWidth / 2 ) );
  }
  
  function segmentEndLeft( segment, lineWidth ) {
    assert && assert( lineWidth !== undefined );
    return segment.end.plus( segment.endTangent.perpendicular().negated().times( lineWidth / 2 ) );
  }
  
  function segmentStartRight( segment, lineWidth ) {
    assert && assert( lineWidth !== undefined );
    return segment.start.plus( segment.startTangent.perpendicular().times( lineWidth / 2 ) );
  }
  
  function segmentEndRight( segment, lineWidth ) {
    assert && assert( lineWidth !== undefined );
    return segment.end.plus( segment.endTangent.perpendicular().times( lineWidth / 2 ) );
  }
  
  return kite.Subpath;
} );

// generated from svgPath.pegjs, with added kite namespace and require.js compatibility

define('KITE/../parser/svgPath',['require','KITE/kite'], function( require ) {
  
  var kite = require( 'KITE/kite' );
  
  /*
   * Generated by PEG.js 0.7.0.
   *
   * http://pegjs.majda.cz/
   */
  
  function quote(s) {
    /*
     * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a
     * string literal except for the closing quote character, backslash,
     * carriage return, line separator, paragraph separator, and line feed.
     * Any character may appear in the form of an escape sequence.
     *
     * For portability, we also escape escape all control and non-ASCII
     * characters. Note that "\0" and "\v" escape sequences are not used
     * because JSHint does not like the first and IE the second.
     */
     return '"' + s
      .replace(/\\/g, '\\\\')  // backslash
      .replace(/"/g, '\\"')    // closing quote character
      .replace(/\x08/g, '\\b') // backspace
      .replace(/\t/g, '\\t')   // horizontal tab
      .replace(/\n/g, '\\n')   // line feed
      .replace(/\f/g, '\\f')   // form feed
      .replace(/\r/g, '\\r')   // carriage return
      .replace(/[\x00-\x07\x0B\x0E-\x1F\x80-\uFFFF]/g, escape)
      + '"';
  }
  
  kite.svgPath = {
    /*
     * Parses the input with a generated parser. If the parsing is successfull,
     * returns a value explicitly or implicitly specified by the grammar from
     * which the parser was generated (see |PEG.buildParser|). If the parsing is
     * unsuccessful, throws |PEG.parser.SyntaxError| describing the error.
     */
    parse: function(input, startRule) {
      var parseFunctions = {
        "svgPath": parse_svgPath,
        "movetoDrawtoCommandGroups": parse_movetoDrawtoCommandGroups,
        "movetoDrawtoCommandGroup": parse_movetoDrawtoCommandGroup,
        "drawtoCommands": parse_drawtoCommands,
        "drawtoCommand": parse_drawtoCommand,
        "moveto": parse_moveto,
        "movetoArgumentSequence": parse_movetoArgumentSequence,
        "closepath": parse_closepath,
        "lineto": parse_lineto,
        "linetoArgumentSequence": parse_linetoArgumentSequence,
        "horizontalLineto": parse_horizontalLineto,
        "horizontalLinetoArgumentSequence": parse_horizontalLinetoArgumentSequence,
        "verticalLineto": parse_verticalLineto,
        "verticalLinetoArgumentSequence": parse_verticalLinetoArgumentSequence,
        "curveto": parse_curveto,
        "curvetoArgumentSequence": parse_curvetoArgumentSequence,
        "curvetoArgument": parse_curvetoArgument,
        "smoothCurveto": parse_smoothCurveto,
        "smoothCurvetoArgumentSequence": parse_smoothCurvetoArgumentSequence,
        "smoothCurvetoArgument": parse_smoothCurvetoArgument,
        "quadraticBezierCurveto": parse_quadraticBezierCurveto,
        "quadraticBezierCurvetoArgumentSequence": parse_quadraticBezierCurvetoArgumentSequence,
        "quadraticBezierCurvetoArgument": parse_quadraticBezierCurvetoArgument,
        "smoothQuadraticBezierCurveto": parse_smoothQuadraticBezierCurveto,
        "smoothQuadraticBezierCurvetoArgumentSequence": parse_smoothQuadraticBezierCurvetoArgumentSequence,
        "ellipticalArc": parse_ellipticalArc,
        "ellipticalArcArgumentSequence": parse_ellipticalArcArgumentSequence,
        "ellipticalArcArgument": parse_ellipticalArcArgument,
        "coordinatePair": parse_coordinatePair,
        "nonnegativeNumber": parse_nonnegativeNumber,
        "number": parse_number,
        "flag": parse_flag,
        "commaWsp": parse_commaWsp,
        "comma": parse_comma,
        "floatingPointConstant": parse_floatingPointConstant,
        "fractionalConstant": parse_fractionalConstant,
        "exponent": parse_exponent,
        "sign": parse_sign,
        "digitSequence": parse_digitSequence,
        "digit": parse_digit,
        "wsp": parse_wsp
      };
      
      if (startRule !== undefined) {
        if (parseFunctions[startRule] === undefined) {
          throw new Error("Invalid rule name: " + quote(startRule) + ".");
        }
      } else {
        startRule = "svgPath";
      }
      
      var pos = 0;
      var reportFailures = 0;
      var rightmostFailuresPos = 0;
      var rightmostFailuresExpected = [];
      
      function padLeft(input, padding, length) {
        var result = input;
        
        var padLength = length - input.length;
        for (var i = 0; i < padLength; i++) {
          result = padding + result;
        }
        
        return result;
      }
      
      function escape(ch) {
        var charCode = ch.charCodeAt(0);
        var escapeChar;
        var length;
        
        if (charCode <= 0xFF) {
          escapeChar = 'x';
          length = 2;
        } else {
          escapeChar = 'u';
          length = 4;
        }
        
        return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length);
      }
      
      function matchFailed(failure) {
        if (pos < rightmostFailuresPos) {
          return;
        }
        
        if (pos > rightmostFailuresPos) {
          rightmostFailuresPos = pos;
          rightmostFailuresExpected = [];
        }
        
        rightmostFailuresExpected.push(failure);
      }
      
      function parse_svgPath() {
        var result0, result1, result2, result3;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = [];
        result1 = parse_wsp();
        while (result1 !== null) {
          result0.push(result1);
          result1 = parse_wsp();
        }
        if (result0 !== null) {
          result1 = parse_movetoDrawtoCommandGroups();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = [];
            result3 = parse_wsp();
            while (result3 !== null) {
              result2.push(result3);
              result3 = parse_wsp();
            }
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, path) { return path ? path : []; })(pos0, result0[1]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_movetoDrawtoCommandGroups() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_movetoDrawtoCommandGroup();
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_movetoDrawtoCommandGroups();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return a.concat( b ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_movetoDrawtoCommandGroup();
          if (result0 !== null) {
            result0 = (function(offset, a) { return a; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_movetoDrawtoCommandGroup() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_moveto();
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_drawtoCommands();
            result2 = result2 !== null ? result2 : "";
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, m, c) { return c.length ? m.concat( c ) : m; })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_drawtoCommands() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_drawtoCommand();
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_drawtoCommands();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, cmd, cmds) { return cmd.concat( cmds ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_drawtoCommand();
          if (result0 !== null) {
            result0 = (function(offset, cmd) { return cmd; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_drawtoCommand() {
        var result0;
        
        result0 = parse_closepath();
        if (result0 === null) {
          result0 = parse_lineto();
          if (result0 === null) {
            result0 = parse_horizontalLineto();
            if (result0 === null) {
              result0 = parse_verticalLineto();
              if (result0 === null) {
                result0 = parse_curveto();
                if (result0 === null) {
                  result0 = parse_smoothCurveto();
                  if (result0 === null) {
                    result0 = parse_quadraticBezierCurveto();
                    if (result0 === null) {
                      result0 = parse_smoothQuadraticBezierCurveto();
                      if (result0 === null) {
                        result0 = parse_ellipticalArc();
                      }
                    }
                  }
                }
              }
            }
          }
        }
        return result0;
      }
      
      function parse_moveto() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 77) {
          result0 = "M";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"M\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_movetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return createMoveTo( args, false ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 109) {
            result0 = "m";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"m\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_movetoArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return createMoveTo( args, true ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_movetoArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_coordinatePair();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_linetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, pair, list) { return [pair].concat( list ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_coordinatePair();
          if (result0 !== null) {
            result0 = (function(offset, pair) { return [pair]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_closepath() {
        var result0;
        var pos0;
        
        pos0 = pos;
        if (input.charCodeAt(pos) === 90) {
          result0 = "Z";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"Z\"");
          }
        }
        if (result0 === null) {
          if (input.charCodeAt(pos) === 122) {
            result0 = "z";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"z\"");
            }
          }
        }
        if (result0 !== null) {
          result0 = (function(offset, command) { return { cmd: 'close' }; })(pos0, result0);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_lineto() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 76) {
          result0 = "L";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"L\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_linetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'lineTo', args: [ arg.x, arg.y ] }; } ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 108) {
            result0 = "l";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"l\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_linetoArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'lineToRelative', args: [ arg.x, arg.y ] }; } ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_linetoArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_coordinatePair();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_linetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return [a].concat( b ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_coordinatePair();
          if (result0 !== null) {
            result0 = (function(offset, a) { return [a]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_horizontalLineto() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 72) {
          result0 = "H";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"H\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_horizontalLinetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'horizontalLineTo', args: [ arg ] } } ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 104) {
            result0 = "h";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"h\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_horizontalLinetoArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'horizontalLineToRelative', args: [ arg ] } } ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_horizontalLinetoArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_number();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_horizontalLinetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return [a].concat( b ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_number();
          if (result0 !== null) {
            result0 = (function(offset, a) { return [a]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_verticalLineto() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 86) {
          result0 = "V";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"V\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_verticalLinetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'verticalLineTo', args: [ arg ] } } ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 118) {
            result0 = "v";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"v\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_verticalLinetoArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'verticalLineToRelative', args: [ arg ] } } ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_verticalLinetoArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_number();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_verticalLinetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return [a].concat( b ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_number();
          if (result0 !== null) {
            result0 = (function(offset, a) { return [a]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_curveto() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 67) {
          result0 = "C";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"C\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_curvetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'cubicCurveTo', args: arg } } ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 99) {
            result0 = "c";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"c\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_curvetoArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'cubicCurveToRelative', args: arg } } ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_curvetoArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_curvetoArgument();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_curvetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, list) { return [a].concat( list ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_curvetoArgument();
          if (result0 !== null) {
            result0 = (function(offset, a) { return [a]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_curvetoArgument() {
        var result0, result1, result2, result3, result4;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_coordinatePair();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_coordinatePair();
            if (result2 !== null) {
              result3 = parse_commaWsp();
              result3 = result3 !== null ? result3 : "";
              if (result3 !== null) {
                result4 = parse_coordinatePair();
                if (result4 !== null) {
                  result0 = [result0, result1, result2, result3, result4];
                } else {
                  result0 = null;
                  pos = pos1;
                }
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b, c) { return [ a.x, a.y, b.x, b.y, c.x, c.y ]; })(pos0, result0[0], result0[2], result0[4]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_smoothCurveto() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 83) {
          result0 = "S";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"S\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_smoothCurvetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'smoothCubicCurveTo', args: arg } } ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 115) {
            result0 = "s";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"s\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_smoothCurvetoArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'smoothCubicCurveToRelative', args: arg } } ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_smoothCurvetoArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_smoothCurvetoArgument();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_smoothCurvetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, list) { return [a].concat( list ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_smoothCurvetoArgument();
          if (result0 !== null) {
            result0 = (function(offset, a) { return [a]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_smoothCurvetoArgument() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_coordinatePair();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_coordinatePair();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return [ a.x, a.y, b.x, b.y ]; })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_quadraticBezierCurveto() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 81) {
          result0 = "Q";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"Q\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_quadraticBezierCurvetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'quadraticCurveTo', args: arg } } ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 113) {
            result0 = "q";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"q\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_quadraticBezierCurvetoArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'quadraticCurveToRelative', args: arg } } ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_quadraticBezierCurvetoArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_quadraticBezierCurvetoArgument();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_quadraticBezierCurvetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, list) { return [a].concat( list ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_quadraticBezierCurvetoArgument();
          if (result0 !== null) {
            result0 = (function(offset, a) { return [a]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_quadraticBezierCurvetoArgument() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_coordinatePair();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_coordinatePair();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return [ a.x, a.y, b.x, b.y ]; })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_smoothQuadraticBezierCurveto() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 84) {
          result0 = "T";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"T\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_smoothQuadraticBezierCurvetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'smoothQuadraticCurveTo', args: [ arg.x, arg.y ] } } ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 116) {
            result0 = "t";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"t\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_smoothQuadraticBezierCurvetoArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'smoothQuadraticCurveToRelative', args: [ arg.x, arg.y ] } } ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_smoothQuadraticBezierCurvetoArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_coordinatePair();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_smoothQuadraticBezierCurvetoArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, list) { return [a].concat( list ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_coordinatePair();
          if (result0 !== null) {
            result0 = (function(offset, a) { return [a]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_ellipticalArc() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 65) {
          result0 = "A";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"A\"");
          }
        }
        if (result0 !== null) {
          result1 = [];
          result2 = parse_wsp();
          while (result2 !== null) {
            result1.push(result2);
            result2 = parse_wsp();
          }
          if (result1 !== null) {
            result2 = parse_ellipticalArcArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'ellipticalArcTo', args: arg } } ); })(pos0, result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 97) {
            result0 = "a";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"a\"");
            }
          }
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result2 = parse_ellipticalArcArgumentSequence();
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, args) { return args.map( function( arg ) { return { cmd: 'ellipticalArcToRelative', args: arg } } ); })(pos0, result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_ellipticalArcArgumentSequence() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_ellipticalArcArgument();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_ellipticalArcArgumentSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, list) { return [a].concat( list ); })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_ellipticalArcArgument();
          if (result0 !== null) {
            result0 = (function(offset, a) { return [a]; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_ellipticalArcArgument() {
        var result0, result1, result2, result3, result4, result5, result6, result7, result8, result9, result10;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_nonnegativeNumber();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_nonnegativeNumber();
            if (result2 !== null) {
              result3 = parse_commaWsp();
              result3 = result3 !== null ? result3 : "";
              if (result3 !== null) {
                result4 = parse_number();
                if (result4 !== null) {
                  result5 = parse_commaWsp();
                  if (result5 !== null) {
                    result6 = parse_flag();
                    if (result6 !== null) {
                      result7 = parse_commaWsp();
                      result7 = result7 !== null ? result7 : "";
                      if (result7 !== null) {
                        result8 = parse_flag();
                        if (result8 !== null) {
                          result9 = parse_commaWsp();
                          result9 = result9 !== null ? result9 : "";
                          if (result9 !== null) {
                            result10 = parse_coordinatePair();
                            if (result10 !== null) {
                              result0 = [result0, result1, result2, result3, result4, result5, result6, result7, result8, result9, result10];
                            } else {
                              result0 = null;
                              pos = pos1;
                            }
                          } else {
                            result0 = null;
                            pos = pos1;
                          }
                        } else {
                          result0 = null;
                          pos = pos1;
                        }
                      } else {
                        result0 = null;
                        pos = pos1;
                      }
                    } else {
                      result0 = null;
                      pos = pos1;
                    }
                  } else {
                    result0 = null;
                    pos = pos1;
                  }
                } else {
                  result0 = null;
                  pos = pos1;
                }
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, rx, ry, rot, largeArc, sweep, to) { return [ rx, ry, rot, largeArc, sweep, to.x, to.y ] })(pos0, result0[0], result0[2], result0[4], result0[6], result0[8], result0[10]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_coordinatePair() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_number();
        if (result0 !== null) {
          result1 = parse_commaWsp();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_number();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return { x: a, y: b }; })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_nonnegativeNumber() {
        var result0;
        var pos0;
        
        pos0 = pos;
        result0 = parse_floatingPointConstant();
        if (result0 !== null) {
          result0 = (function(offset, number) { return parseFloat( number, 10 ); })(pos0, result0);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_digitSequence();
          if (result0 !== null) {
            result0 = (function(offset, number) { return parseInt( number, 10 ); })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_number() {
        var result0, result1;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_sign();
        result0 = result0 !== null ? result0 : "";
        if (result0 !== null) {
          result1 = parse_floatingPointConstant();
          if (result1 !== null) {
            result0 = [result0, result1];
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, sign, number) { return parseFloat( sign + number, 10 ); })(pos0, result0[0], result0[1]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          result0 = parse_sign();
          result0 = result0 !== null ? result0 : "";
          if (result0 !== null) {
            result1 = parse_digitSequence();
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, sign, number) { return parseInt( sign + number, 10 ); })(pos0, result0[0], result0[1]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_flag() {
        var result0;
        var pos0;
        
        pos0 = pos;
        if (input.charCodeAt(pos) === 48) {
          result0 = "0";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"0\"");
          }
        }
        if (result0 !== null) {
          result0 = (function(offset) { return false; })(pos0);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          if (input.charCodeAt(pos) === 49) {
            result0 = "1";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"1\"");
            }
          }
          if (result0 !== null) {
            result0 = (function(offset) { return true; })(pos0);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_commaWsp() {
        var result0, result1, result2, result3;
        var pos0;
        
        pos0 = pos;
        result1 = parse_wsp();
        if (result1 !== null) {
          result0 = [];
          while (result1 !== null) {
            result0.push(result1);
            result1 = parse_wsp();
          }
        } else {
          result0 = null;
        }
        if (result0 !== null) {
          result1 = parse_comma();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = [];
            result3 = parse_wsp();
            while (result3 !== null) {
              result2.push(result3);
              result3 = parse_wsp();
            }
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos0;
            }
          } else {
            result0 = null;
            pos = pos0;
          }
        } else {
          result0 = null;
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          result0 = parse_comma();
          if (result0 !== null) {
            result1 = [];
            result2 = parse_wsp();
            while (result2 !== null) {
              result1.push(result2);
              result2 = parse_wsp();
            }
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos0;
            }
          } else {
            result0 = null;
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_comma() {
        var result0;
        
        if (input.charCodeAt(pos) === 44) {
          result0 = ",";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\",\"");
          }
        }
        return result0;
      }
      
      function parse_floatingPointConstant() {
        var result0, result1;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_fractionalConstant();
        if (result0 !== null) {
          result1 = parse_exponent();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result0 = [result0, result1];
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return a + b; })(pos0, result0[0], result0[1]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          result0 = parse_digitSequence();
          if (result0 !== null) {
            result1 = parse_exponent();
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, a, b) { return a + b; })(pos0, result0[0], result0[1]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_fractionalConstant() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_digitSequence();
        result0 = result0 !== null ? result0 : "";
        if (result0 !== null) {
          if (input.charCodeAt(pos) === 46) {
            result1 = ".";
            pos++;
          } else {
            result1 = null;
            if (reportFailures === 0) {
              matchFailed("\".\"");
            }
          }
          if (result1 !== null) {
            result2 = parse_digitSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return a + '.' + b; })(pos0, result0[0], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          pos0 = pos;
          pos1 = pos;
          result0 = parse_digitSequence();
          if (result0 !== null) {
            if (input.charCodeAt(pos) === 46) {
              result1 = ".";
              pos++;
            } else {
              result1 = null;
              if (reportFailures === 0) {
                matchFailed("\".\"");
              }
            }
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, a) { return a })(pos0, result0[0]);
          }
          if (result0 === null) {
            pos = pos0;
          }
        }
        return result0;
      }
      
      function parse_exponent() {
        var result0, result1, result2;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        if (input.charCodeAt(pos) === 101) {
          result0 = "e";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"e\"");
          }
        }
        if (result0 === null) {
          if (input.charCodeAt(pos) === 69) {
            result0 = "E";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"E\"");
            }
          }
        }
        if (result0 !== null) {
          result1 = parse_sign();
          result1 = result1 !== null ? result1 : "";
          if (result1 !== null) {
            result2 = parse_digitSequence();
            if (result2 !== null) {
              result0 = [result0, result1, result2];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b, c) { return a + b + c; })(pos0, result0[0], result0[1], result0[2]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        return result0;
      }
      
      function parse_sign() {
        var result0;
        
        if (input.charCodeAt(pos) === 43) {
          result0 = "+";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\"+\"");
          }
        }
        if (result0 === null) {
          if (input.charCodeAt(pos) === 45) {
            result0 = "-";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"-\"");
            }
          }
        }
        return result0;
      }
      
      function parse_digitSequence() {
        var result0, result1;
        var pos0, pos1;
        
        pos0 = pos;
        pos1 = pos;
        result0 = parse_digit();
        if (result0 !== null) {
          result1 = parse_digitSequence();
          if (result1 !== null) {
            result0 = [result0, result1];
          } else {
            result0 = null;
            pos = pos1;
          }
        } else {
          result0 = null;
          pos = pos1;
        }
        if (result0 !== null) {
          result0 = (function(offset, a, b) { return a + b; })(pos0, result0[0], result0[1]);
        }
        if (result0 === null) {
          pos = pos0;
        }
        if (result0 === null) {
          result0 = parse_digit();
        }
        return result0;
      }
      
      function parse_digit() {
        var result0;
        
        if (/^[0-9]/.test(input.charAt(pos))) {
          result0 = input.charAt(pos);
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("[0-9]");
          }
        }
        return result0;
      }
      
      function parse_wsp() {
        var result0;
        
        if (input.charCodeAt(pos) === 32) {
          result0 = " ";
          pos++;
        } else {
          result0 = null;
          if (reportFailures === 0) {
            matchFailed("\" \"");
          }
        }
        if (result0 === null) {
          if (input.charCodeAt(pos) === 9) {
            result0 = "\t";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"\\t\"");
            }
          }
          if (result0 === null) {
            if (input.charCodeAt(pos) === 13) {
              result0 = "\r";
              pos++;
            } else {
              result0 = null;
              if (reportFailures === 0) {
                matchFailed("\"\\r\"");
              }
            }
            if (result0 === null) {
              if (input.charCodeAt(pos) === 10) {
                result0 = "\n";
                pos++;
              } else {
                result0 = null;
                if (reportFailures === 0) {
                  matchFailed("\"\\n\"");
                }
              }
            }
          }
        }
        return result0;
      }
      
      
      function cleanupExpected(expected) {
        expected.sort();
        
        var lastExpected = null;
        var cleanExpected = [];
        for (var i = 0; i < expected.length; i++) {
          if (expected[i] !== lastExpected) {
            cleanExpected.push(expected[i]);
            lastExpected = expected[i];
          }
        }
        return cleanExpected;
      }
      
      function computeErrorPosition() {
        /*
         * The first idea was to use |String.split| to break the input up to the
         * error position along newlines and derive the line and column from
         * there. However IE's |split| implementation is so broken that it was
         * enough to prevent it.
         */
        
        var line = 1;
        var column = 1;
        var seenCR = false;
        
        for (var i = 0; i < Math.max(pos, rightmostFailuresPos); i++) {
          var ch = input.charAt(i);
          if (ch === "\n") {
            if (!seenCR) { line++; }
            column = 1;
            seenCR = false;
          } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") {
            line++;
            column = 1;
            seenCR = true;
          } else {
            column++;
            seenCR = false;
          }
        }
        
        return { line: line, column: column };
      }
      
      
        function createMoveTo( args, isRelative ) {
          var result = [ {
            cmd: isRelative ? 'moveToRelative' : 'moveTo',
            args: [ args[0].x, args[0].y ]
          } ];
          
          // any other coordinate pairs are implicit lineTos
          if ( args.length > 1 ) {
            for ( var i = 1; i < args.length; i++ ) {
              result.push( {
                cmd: isRelative ? 'lineToRelative' : 'lineTo',
                args: [ args[i].x, args[i].y ]
              } );
            }
          }
          return result;
        }
      
      
      var result = parseFunctions[startRule]();
      
      /*
       * The parser is now in one of the following three states:
       *
       * 1. The parser successfully parsed the whole input.
       *
       *    - |result !== null|
       *    - |pos === input.length|
       *    - |rightmostFailuresExpected| may or may not contain something
       *
       * 2. The parser successfully parsed only a part of the input.
       *
       *    - |result !== null|
       *    - |pos < input.length|
       *    - |rightmostFailuresExpected| may or may not contain something
       *
       * 3. The parser did not successfully parse any part of the input.
       *
       *   - |result === null|
       *   - |pos === 0|
       *   - |rightmostFailuresExpected| contains at least one failure
       *
       * All code following this comment (including called functions) must
       * handle these states.
       */
      if (result === null || pos !== input.length) {
        var offset = Math.max(pos, rightmostFailuresPos);
        var found = offset < input.length ? input.charAt(offset) : null;
        var errorPosition = computeErrorPosition();
        
        throw new this.SyntaxError(
          cleanupExpected(rightmostFailuresExpected),
          found,
          offset,
          errorPosition.line,
          errorPosition.column
        );
      }
      
      return result;
    },
    
    /* Returns the parser source code. */
    toSource: function() { return this._source; }
  };
  var result = kite.svgPath;
  
  /* Thrown when a parser encounters a syntax error. */
  
  result.SyntaxError = function(expected, found, offset, line, column) {
    function buildMessage(expected, found) {
      var expectedHumanized, foundHumanized;
      
      switch (expected.length) {
        case 0:
          expectedHumanized = "end of input";
          break;
        case 1:
          expectedHumanized = expected[0];
          break;
        default:
          expectedHumanized = expected.slice(0, expected.length - 1).join(", ")
            + " or "
            + expected[expected.length - 1];
      }
      
      foundHumanized = found ? quote(found) : "end of input";
      
      return "Expected " + expectedHumanized + " but " + foundHumanized + " found.";
    }
    
    this.name = "SyntaxError";
    this.expected = expected;
    this.found = found;
    this.message = buildMessage(expected, found);
    this.offset = offset;
    this.line = line;
    this.column = column;
  };
  
  result.SyntaxError.prototype = Error.prototype;
  
  return result;
});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Styles needed to determine a stroked line shape.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/util/LineStyles',['require','ASSERT/assert','KITE/kite'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );
  
  var kite = require( 'KITE/kite' );
  
  kite.LineStyles = function( args ) {
    if ( args === undefined ) {
      args = {};
    }
    this.lineWidth = args.lineWidth !== undefined ? args.lineWidth : 1;
    this.lineCap = args.lineCap !== undefined ? args.lineCap : 'butt'; // butt, round, square
    this.lineJoin = args.lineJoin !== undefined ? args.lineJoin : 'miter'; // miter, round, bevel
    this.lineDash = args.lineDash ? args.lineDash : []; // [] is default, otherwise an array of numbers
    this.lineDashOffset = args.lineDashOffset !== undefined ? args.lineDashOffset : 0; // 0 default, any number
    this.miterLimit = args.miterLimit !== undefined ? args.miterLimit : 10; // see https://svgwg.org/svg2-draft/painting.html for miterLimit computations
    
    assert && assert( Array.isArray( this.lineDash ) );
  };
  var LineStyles = kite.LineStyles;
  LineStyles.prototype = {
    constructor: LineStyles,
    
    equals: function( other ) {
      var typical = this.lineWidth === other.lineWidth &&
                    this.lineCap === other.lineCap &&
                    this.lineJoin === other.lineJoin &&
                    this.miterLimit === other.miterLimit &&
                    this.lineDashOffset === other.lineDashOffset;
      if ( !typical ) {
        return false;
      }
      
      if ( this.lineDash.length === other.lineDash.length ) {
        for ( var i = 0; i < this.lineDash.length; i++ ) {
          if ( this.lineDash[i] !== other.lineDash[i] ) {
            return false;
          }
        }
      } else {
        // line dashes must be different
        return false;
      }
      
      return true;
    }
  };
  
  return kite.LineStyles;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Quadratic Bezier segment
 *
 * Good reference: http://cagd.cs.byu.edu/~557/text/ch2.pdf
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/segments/Quadratic',['require','ASSERT/assert','KITE/kite','PHET_CORE/inherit','DOT/Bounds2','DOT/Matrix3','DOT/Util','KITE/segments/Segment'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );

  var kite = require( 'KITE/kite' );
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var solveQuadraticRootsReal = require( 'DOT/Util' ).solveQuadraticRootsReal;

  var Segment = require( 'KITE/segments/Segment' );

  Segment.Quadratic = function Quadratic( start, control, end, skipComputations ) {
    this.start = start;
    this.control = control;
    this.end = end;
    
    if ( start.equals( end ) && start.equals( control ) ) {
      this.invalid = true;
      return;
    }
    
    var t;
    
    // allows us to skip unnecessary computation in the subdivision steps
    if ( skipComputations ) {
      return;
    }
    
    var controlIsStart = start.equals( control );
    var controlIsEnd = end.equals( control );
    // ensure the points are distinct
    assert && assert( !controlIsStart || !controlIsEnd );
    
    // allow either the start or end point to be the same as the control point (necessary if you do a quadraticCurveTo on an empty path)
    // tangents go through the control point, which simplifies things
    this.startTangent = controlIsStart ? end.minus( start ).normalized() : control.minus( start ).normalized();
    this.endTangent = controlIsEnd ? end.minus( start ).normalized() : end.minus( control ).normalized();
    
    // calculate our temporary guaranteed lower bounds based on the end points
    this.bounds = new Bounds2( Math.min( start.x, end.x ), Math.min( start.y, end.y ), Math.max( start.x, end.x ), Math.max( start.y, end.y ) );
    
    // compute x and y where the derivative is 0, so we can include this in the bounds
    var divisorX = 2 * ( end.x - 2 * control.x + start.x );
    if ( divisorX !== 0 ) {
      this.tCriticalX = -2 * ( control.x - start.x ) / divisorX;
      
      if ( t > 0 && t < 1 ) {
        this.bounds = this.bounds.withPoint( this.positionAt( this.tCriticalX ) );
      }
    }
    var divisorY = 2 * ( end.y - 2 * control.y + start.y );
    if ( divisorY !== 0 ) {
      this.tCriticalY = -2 * ( control.y - start.y ) / divisorY;
      
      if ( t > 0 && t < 1 ) {
        this.bounds = this.bounds.withPoint( this.positionAt( this.tCriticalY ) );
      }
    }
  };
  inherit( Segment, Segment.Quadratic, {
    
    degree: 2,
    
    // can be described from t=[0,1] as: (1-t)^2 start + 2(1-t)t control + t^2 end
    positionAt: function( t ) {
      var mt = 1 - t;
      return this.start.times( mt * mt ).plus( this.control.times( 2 * mt * t ) ).plus( this.end.times( t * t ) );
    },
    
    // derivative: 2(1-t)( control - start ) + 2t( end - control )
    tangentAt: function( t ) {
      return this.control.minus( this.start ).times( 2 * ( 1 - t ) ).plus( this.end.minus( this.control ).times( 2 * t ) );
    },
    
    curvatureAt: function( t ) {
      // see http://cagd.cs.byu.edu/~557/text/ch2.pdf p31
      // TODO: remove code duplication with Cubic
      var epsilon = 0.0000001;
      if ( Math.abs( t - 0.5 ) > 0.5 - epsilon ) {
        var isZero = t < 0.5;
        var p0 = isZero ? this.start : this.end;
        var p1 = this.control;
        var p2 = isZero ? this.end : this.start;
        var d10 = p1.minus( p0 );
        var a = d10.magnitude();
        var h = ( isZero ? -1 : 1 ) * d10.perpendicular().normalized().dot( p2.minus( p1 ) );
        return ( h * ( this.degree - 1 ) ) / ( this.degree * a * a );
      } else {
        return this.subdivided( t, true )[0].curvatureAt( 1 );
      }
    },
    
    // see http://www.visgraf.impa.br/sibgrapi96/trabs/pdf/a14.pdf
    // and http://math.stackexchange.com/questions/12186/arc-length-of-bezier-curves for curvature / arc length
    
    offsetTo: function( r, reverse ) {
      // TODO: implement more accurate method at http://www.antigrain.com/research/adaptive_bezier/index.html
      // TODO: or more recently (and relevantly): http://www.cis.usouthal.edu/~hain/general/Publications/Bezier/BezierFlattening.pdf
      var curves = [this];
      
      // subdivide this curve
      var depth = 5; // generates 2^depth curves
      for ( var i = 0; i < depth; i++ ) {
        curves = _.flatten( _.map( curves, function( curve ) {
          return curve.subdivided( 0.5, true );
        } ));
      }
      
      var offsetCurves = _.map( curves, function( curve ) { return curve.approximateOffset( r ); } );
      
      if ( reverse ) {
        offsetCurves.reverse();
        offsetCurves = _.map( offsetCurves, function( curve ) { return curve.reversed( true ); } );
      }
      
      return offsetCurves;
    },
    
    subdivided: function( t, skipComputations ) {
      // de Casteljau method
      var leftMid = this.start.blend( this.control, t );
      var rightMid = this.control.blend( this.end, t );
      var mid = leftMid.blend( rightMid, t );
      return [
        new Segment.Quadratic( this.start, leftMid, mid, skipComputations ),
        new Segment.Quadratic( mid, rightMid, this.end, skipComputations )
      ];
    },
    
    reversed: function( skipComputations ) {
      return new Segment.Quadratic( this.end, this.control, this.start );
    },
    
    approximateOffset: function( r ) {
      return new Segment.Quadratic(
        this.start.plus( ( this.start.equals( this.control ) ? this.end.minus( this.start ) : this.control.minus( this.start ) ).perpendicular().normalized().times( r ) ),
        this.control.plus( this.end.minus( this.start ).perpendicular().normalized().times( r ) ),
        this.end.plus( ( this.end.equals( this.control ) ? this.end.minus( this.start ) : this.end.minus( this.control ) ).perpendicular().normalized().times( r ) )
      );
    },
    
    getSVGPathFragment: function() {
      return 'Q ' + this.control.x + ' ' + this.control.y + ' ' + this.end.x + ' ' + this.end.y;
    },
    
    strokeLeft: function( lineWidth ) {
      return this.offsetTo( -lineWidth / 2, false );
    },
    
    strokeRight: function( lineWidth ) {
      return this.offsetTo( lineWidth / 2, true );
    },
    
    getInteriorExtremaTs: function() {
      var result = [];
      var epsilon = 0.0000000001; // TODO: general kite epsilon?
      if ( this.tCriticalX !== undefined && this.tCriticalX > epsilon && this.tCriticalX < 1 - epsilon ) {
        result.push( this.tCriticalX );
      }
      if ( this.tCriticalY !== undefined && this.tCriticalY > epsilon && this.tCriticalY < 1 - epsilon ) {
        result.push( this.tCriticalY );
      }
      return result.sort();
    },
    
    intersectsBounds: function( bounds ) {
      throw new Error( 'Segment.Quadratic.intersectsBounds unimplemented' ); // TODO: implement
    },
    
    // returns the resultant winding number of this ray intersecting this segment.
    intersection: function( ray ) {
      var self = this;
      var result = [];
      
      // find the rotation that will put our ray in the direction of the x-axis so we can only solve for y=0 for intersections
      var inverseMatrix = Matrix3.rotation2( -ray.dir.angle() ).timesMatrix( Matrix3.translation( -ray.pos.x, -ray.pos.y ) );
      
      var p0 = inverseMatrix.timesVector2( this.start );
      var p1 = inverseMatrix.timesVector2( this.control );
      var p2 = inverseMatrix.timesVector2( this.end );
      
      //(1-t)^2 start + 2(1-t)t control + t^2 end
      var a = p0.y - 2 * p1.y + p2.y;
      var b = -2 * p0.y + 2 * p1.y;
      var c = p0.y;
      
      var ts = solveQuadraticRootsReal( a, b, c );
      
      _.each( ts, function( t ) {
        if ( t >= 0 && t <= 1 ) {
          var hitPoint = self.positionAt( t );
          var unitTangent = self.tangentAt( t ).normalized();
          var perp = unitTangent.perpendicular();
          var toHit = hitPoint.minus( ray.pos );
          
          // make sure it's not behind the ray
          if ( toHit.dot( ray.dir ) > 0 ) {
            result.push( {
              distance: toHit.magnitude(),
              point: hitPoint,
              normal: perp.dot( ray.dir ) > 0 ? perp.negated() : perp,
              wind: ray.dir.perpendicular().dot( unitTangent ) < 0 ? 1 : -1
            } );
          }
        }
      } );
      return result;
    },
    
    windingIntersection: function( ray ) {
      var wind = 0;
      var hits = this.intersection( ray );
      _.each( hits, function( hit ) {
        wind += hit.wind;
      } );
      return wind;
    },
    
    // assumes the current position is at start
    writeToContext: function( context ) {
      context.quadraticCurveTo( this.control.x, this.control.y, this.end.x, this.end.y );
    },
    
    transformed: function( matrix ) {
      return new Segment.Quadratic( matrix.timesVector2( this.start ), matrix.timesVector2( this.control ), matrix.timesVector2( this.end ) );
    }
  } );
  
  return Segment.Quadratic;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Cubic Bezier segment.
 *
 * See http://www.cis.usouthal.edu/~hain/general/Publications/Bezier/BezierFlattening.pdf for info
 *
 * Good reference: http://cagd.cs.byu.edu/~557/text/ch2.pdf
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/segments/Cubic',['require','ASSERT/assert','KITE/kite','PHET_CORE/inherit','DOT/Bounds2','DOT/Vector2','DOT/Matrix3','DOT/Util','DOT/Util','KITE/segments/Segment','KITE/segments/Quadratic'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );

  var kite = require( 'KITE/kite' );
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Vector2 = require( 'DOT/Vector2' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var solveQuadraticRootsReal = require( 'DOT/Util' ).solveQuadraticRootsReal;
  var solveCubicRootsReal = require( 'DOT/Util' ).solveCubicRootsReal;
  
  var Segment = require( 'KITE/segments/Segment' );
  require( 'KITE/segments/Quadratic' );

  Segment.Cubic = function Cubic( start, control1, control2, end, skipComputations ) {
    this.start = start;
    this.control1 = control1;
    this.control2 = control2;
    this.end = end;
    
    // allows us to skip unnecessary computation in the subdivision steps
    if ( skipComputations ) {
      return;
    }
    
    if ( start.equals( end, 0 ) && start.equals( control1, 0 ) && start.equals( control2, 0 ) ) {
      this.invalid = true;
      return;
    }
    
    this.startTangent = this.tangentAt( 0 ).normalized();
    this.endTangent = this.tangentAt( 1 ).normalized();
    
    // from http://www.cis.usouthal.edu/~hain/general/Publications/Bezier/BezierFlattening.pdf
    this.r = control1.minus( start ).normalized();
    this.s = this.r.perpendicular();
    
    var a = start.times( -1 ).plus( control1.times( 3 ) ).plus( control2.times( -3 ) ).plus( end );
    var b = start.times( 3 ).plus( control1.times( -6 ) ).plus( control2.times( 3 ) );
    var c = start.times( -3 ).plus( control1.times( 3 ) );
    var d = start;
    
    var aPerp = a.perpendicular();
    var bPerp = b.perpendicular();
    var aPerpDotB = aPerp.dot( b );
    
    this.tCusp = -0.5 * ( aPerp.dot( c ) / aPerpDotB );
    this.tDeterminant = this.tCusp * this.tCusp - ( 1 / 3 ) * ( bPerp.dot( c ) / aPerpDotB );
    if ( this.tDeterminant >= 0 ) {
      var sqrtDet = Math.sqrt( this.tDeterminant );
      this.tInflection1 = this.tCusp - sqrtDet;
      this.tInflection2 = this.tCusp + sqrtDet;
    }
    
    if ( this.hasCusp() ) {
      // if there is a cusp, we'll split at the cusp into two quadratic bezier curves.
      // see http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.94.8088&rep=rep1&type=pdf (Singularities of rational Bezier curves - J Monterde, 2001)
      var subdividedAtCusp = this.subdivided( this.tCusp, true );
      this.startQuadratic = new Segment.Quadratic( subdividedAtCusp[0].start, subdividedAtCusp[0].control1, subdividedAtCusp[0].end, false );
      this.endQuadratic = new Segment.Quadratic( subdividedAtCusp[1].start, subdividedAtCusp[1].control2, subdividedAtCusp[1].end, false );
    }
    
    this.bounds = Bounds2.NOTHING;
    this.bounds = this.bounds.withPoint( this.start );
    this.bounds = this.bounds.withPoint( this.end );
    
    /*---------------------------------------------------------------------------*
    * Bounds
    *----------------------------------------------------------------------------*/
    
    // finds what t values the cubic extrema are at (if any).
    function extremaT( v0, v1, v2, v3 ) {
      // coefficients of derivative
      var a = -3 * v0 + 9 * v1 -9 * v2 + 3 * v3;
      var b =  6 * v0 - 12 * v1 + 6 * v2;
      var c = -3 * v0 + 3 * v1;
      
      return solveQuadraticRootsReal( a, b, c );
    }
    
    var cubic = this;
    this.xExtremaT = extremaT( this.start.x, this.control1.x, this.control2.x, this.end.x );
    _.each( this.xExtremaT, function( t ) {
      if ( t >= 0 && t <= 1 ) {
        cubic.bounds = cubic.bounds.withPoint( cubic.positionAt( t ) );
      }
    } );
    this.yExtremaT = extremaT( this.start.y, this.control1.y, this.control2.y, this.end.y );
    _.each( this.yExtremaT, function( t ) {
      if ( t >= 0 && t <= 1 ) {
        cubic.bounds = cubic.bounds.withPoint( cubic.positionAt( t ) );
      }
    } );
    
    if ( this.hasCusp() ) {
      this.bounds = this.bounds.withPoint( this.positionAt( this.tCusp ) );
    }
  };
  inherit( Segment, Segment.Cubic, {
    
    degree: 3,
    
    hasCusp: function() {
      var epsilon = 0.000001; // TODO: make this available to change?
      return this.tangentAt( this.tCusp ).magnitude() < epsilon && this.tCusp >= 0 && this.tCusp <= 1;
    },
    
    // position: (1 - t)^3*start + 3*(1 - t)^2*t*control1 + 3*(1 - t) t^2*control2 + t^3*end
    positionAt: function( t ) {
      var mt = 1 - t;
      return this.start.times( mt * mt * mt ).plus( this.control1.times( 3 * mt * mt * t ) ).plus( this.control2.times( 3 * mt * t * t ) ).plus( this.end.times( t * t * t ) );
    },
    
    // derivative: -3 p0 (1 - t)^2 + 3 p1 (1 - t)^2 - 6 p1 (1 - t) t + 6 p2 (1 - t) t - 3 p2 t^2 + 3 p3 t^2
    tangentAt: function( t ) {
      var mt = 1 - t;
      return this.start.times( -3 * mt * mt ).plus( this.control1.times( 3 * mt * mt - 6 * mt * t ) ).plus( this.control2.times( 6 * mt * t - 3 * t * t ) ).plus( this.end.times( 3 * t * t ) );
    },
    
    curvatureAt: function( t ) {
      // see http://cagd.cs.byu.edu/~557/text/ch2.pdf p31
      // TODO: remove code duplication with Quadratic
      var epsilon = 0.0000001;
      if ( Math.abs( t - 0.5 ) > 0.5 - epsilon ) {
        var isZero = t < 0.5;
        var p0 = isZero ? this.start : this.end;
        var p1 = isZero ? this.control1 : this.control2;
        var p2 = isZero ? this.control2 : this.control1;
        var d10 = p1.minus( p0 );
        var a = d10.magnitude();
        var h = ( isZero ? -1 : 1 ) * d10.perpendicular().normalized().dot( p2.minus( p1 ) );
        return ( h * ( this.degree - 1 ) ) / ( this.degree * a * a );
      } else {
        return this.subdivided( t, true )[0].curvatureAt( 1 );
      }
    },
    
    toRS: function( point ) {
      var firstVector = point.minus( this.start );
      return new Vector2( firstVector.dot( this.r ), firstVector.dot( this.s ) );
    },
    
    subdivided: function( t, skipComputations ) {
      // de Casteljau method
      // TODO: add a 'bisect' or 'between' method for vectors?
      var left = this.start.blend( this.control1, t );
      var right = this.control2.blend( this.end, t );
      var middle = this.control1.blend( this.control2, t );
      var leftMid = left.blend( middle, t );
      var rightMid = middle.blend( right, t );
      var mid = leftMid.blend( rightMid, t );
      return [
        new Segment.Cubic( this.start, left, leftMid, mid, skipComputations ),
        new Segment.Cubic( mid, rightMid, right, this.end, skipComputations )
      ];
    },
    
    offsetTo: function( r, reverse ) {
      // TODO: implement more accurate method at http://www.antigrain.com/research/adaptive_bezier/index.html
      // TODO: or more recently (and relevantly): http://www.cis.usouthal.edu/~hain/general/Publications/Bezier/BezierFlattening.pdf
      
      // how many segments to create (possibly make this more adaptive?)
      var quantity = 32;
      
      var points = [];
      var result = [];
      for ( var i = 0; i < quantity; i++ ) {
        var t = i / ( quantity - 1 );
        if ( reverse ) {
          t = 1 - t;
        }
        
        points.push( this.positionAt( t ).plus( this.tangentAt( t ).perpendicular().normalized().times( r ) ) );
        if ( i > 0 ) {
          result.push( new Segment.Line( points[i-1], points[i] ) );
        }
      }
      
      return result;
    },
    
    getSVGPathFragment: function() {
      return 'C ' + this.control1.x + ' ' + this.control1.y + ' ' + this.control2.x + ' ' + this.control2.y + ' ' + this.end.x + ' ' + this.end.y;
    },
    
    strokeLeft: function( lineWidth ) {
      return this.offsetTo( -lineWidth / 2, false );
    },
    
    strokeRight: function( lineWidth ) {
      return this.offsetTo( lineWidth / 2, true );
    },
    
    getInteriorExtremaTs: function() {
      var ts = this.xExtremaT.concat( this.yExtremaT );
      var result = [];
      _.each( ts, function( t ) {
        var epsilon = 0.0000000001; // TODO: general kite epsilon?
        if ( t > epsilon && t < 1 - epsilon ) {
          // don't add duplicate t values
          if ( _.every( result, function( otherT ) { return Math.abs( t - otherT ) > epsilon; } ) ) {
            result.push( t );
          }
        }
      } );
      return result.sort();
    },
    
    intersectsBounds: function( bounds ) {
      throw new Error( 'Segment.Cubic.intersectsBounds unimplemented' ); // TODO: implement
    },
    
    // returns the resultant winding number of this ray intersecting this segment.
    intersection: function( ray ) {
      var self = this;
      var result = [];
      
      // find the rotation that will put our ray in the direction of the x-axis so we can only solve for y=0 for intersections
      var inverseMatrix = Matrix3.rotation2( -ray.dir.angle() ).timesMatrix( Matrix3.translation( -ray.pos.x, -ray.pos.y ) );
      
      var p0 = inverseMatrix.timesVector2( this.start );
      var p1 = inverseMatrix.timesVector2( this.control1 );
      var p2 = inverseMatrix.timesVector2( this.control2 );
      var p3 = inverseMatrix.timesVector2( this.end );
      
      // polynomial form of cubic: start + (3 control1 - 3 start) t + (-6 control1 + 3 control2 + 3 start) t^2 + (3 control1 - 3 control2 + end - start) t^3
      var a = -p0.y + 3 * p1.y - 3 * p2.y + p3.y;
      var b = 3 * p0.y - 6 * p1.y + 3 * p2.y;
      var c = -3 * p0.y + 3 * p1.y;
      var d = p0.y;
      
      var ts = solveCubicRootsReal( a, b, c, d );
      
      _.each( ts, function( t ) {
        if ( t >= 0 && t <= 1 ) {
          var hitPoint = self.positionAt( t );
          var unitTangent = self.tangentAt( t ).normalized();
          var perp = unitTangent.perpendicular();
          var toHit = hitPoint.minus( ray.pos );
          
          // make sure it's not behind the ray
          if ( toHit.dot( ray.dir ) > 0 ) {
            result.push( {
              distance: toHit.magnitude(),
              point: hitPoint,
              normal: perp.dot( ray.dir ) > 0 ? perp.negated() : perp,
              wind: ray.dir.perpendicular().dot( unitTangent ) < 0 ? 1 : -1
            } );
          }
        }
      } );
      return result;
    },
    
    windingIntersection: function( ray ) {
      var wind = 0;
      var hits = this.intersection( ray );
      _.each( hits, function( hit ) {
        wind += hit.wind;
      } );
      return wind;
    },
    
    // assumes the current position is at start
    writeToContext: function( context ) {
      context.bezierCurveTo( this.control1.x, this.control1.y, this.control2.x, this.control2.y, this.end.x, this.end.y );
    },
    
    transformed: function( matrix ) {
      return new Segment.Cubic( matrix.timesVector2( this.start ), matrix.timesVector2( this.control1 ), matrix.timesVector2( this.control2 ), matrix.timesVector2( this.end ) );
    }
    
    // returns the resultant winding number of this ray intersecting this segment.
    // windingIntersection: function( ray ) {
    //   // find the rotation that will put our ray in the direction of the x-axis so we can only solve for y=0 for intersections
    //   var inverseMatrix = Matrix3.rotation2( -ray.dir.angle() );
    //   assert && assert( inverseMatrix.timesVector2( ray.dir ).x > 0.99 ); // verify that we transform the unit vector to the x-unit
      
    //   var y0 = inverseMatrix.timesVector2( this.start ).y;
    //   var y1 = inverseMatrix.timesVector2( this.control1 ).y;
    //   var y2 = inverseMatrix.timesVector2( this.control2 ).y;
    //   var y3 = inverseMatrix.timesVector2( this.end ).y;
      
    //   // polynomial form of cubic: start + (3 control1 - 3 start) t + (-6 control1 + 3 control2 + 3 start) t^2 + (3 control1 - 3 control2 + end - start) t^3
    //   var a = -y0 + 3 * y1 - 3 * y2 + y3;
    //   var b = 3 * y0 - 6 * y1 + 3 * y2;
    //   var c = -3 * y0 + 3 * y1;
    //   var d = y0;
      
    //   // solve cubic roots
    //   var ts = solveCubicRootsReal( a, b, c, d );
      
    //   var result = 0;
      
    //   // for each hit
    //   _.each( ts, function( t ) {
    //     if ( t >= 0 && t <= 1 ) {
    //       result += ray.dir.perpendicular().dot( this.tangentAt( t ) ) < 0 ? 1 : -1;
    //     }
    //   } );
      
    //   return result;
    // }
  } );
  
  return Segment.Cubic;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Elliptical arc segment
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/segments/EllipticalArc',['require','ASSERT/assert','KITE/kite','PHET_CORE/inherit','DOT/Vector2','DOT/Bounds2','DOT/Matrix3','DOT/Transform3','DOT/Util','DOT/Util','KITE/segments/Segment','KITE/util/Subpath'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );

  var kite = require( 'KITE/kite' );
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Vector2 = require( 'DOT/Vector2' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var Transform3 = require( 'DOT/Transform3' );
  var toDegrees = require( 'DOT/Util' ).toDegrees;
  var DotUtil = require( 'DOT/Util' );

  var Segment = require( 'KITE/segments/Segment' );
  require( 'KITE/util/Subpath' );

  // TODO: notes at http://www.w3.org/TR/SVG/implnote.html#PathElementImplementationNotes
  // Canvas notes at http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-ellipse
  Segment.EllipticalArc = function EllipticalArc( center, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise ) {
    if ( radiusX < 0 ) {
      // support this case since we might actually need to handle it inside of strokes?
      radiusX = -radiusX;
      startAngle = Math.PI - startAngle;
      endAngle = Math.PI - endAngle;
      anticlockwise = !anticlockwise;
    }
    if ( radiusY < 0 ) {
      // support this case since we might actually need to handle it inside of strokes?
      radiusY = -radiusY;
      startAngle = -startAngle;
      endAngle = -endAngle;
      anticlockwise = !anticlockwise;
    }
    if ( radiusX < radiusY ) {
      // swap radiusX and radiusY internally for consistent Canvas / SVG output
      rotation += Math.PI / 2;
      startAngle -= Math.PI / 2;
      endAngle -= Math.PI / 2;
      
      // swap radiusX and radiusY
      var tmpR = radiusX;
      radiusX = radiusY;
      radiusY = tmpR;
    }
    
    this.center = center;
    this.radiusX = radiusX;
    this.radiusY = radiusY;
    this.rotation = rotation;
    this.startAngle = startAngle;
    this.endAngle = endAngle;
    this.anticlockwise = anticlockwise;
    
    this.unitTransform = Segment.EllipticalArc.computeUnitTransform( center, radiusX, radiusY, rotation );
    
    this.start = this.positionAtAngle( startAngle );
    this.end = this.positionAtAngle( endAngle );
    this.startTangent = this.tangentAtAngle( startAngle ).normalized();
    this.endTangent = this.tangentAtAngle( endAngle ).normalized();
    
    if ( radiusX === 0 || radiusY === 0 || startAngle === endAngle ) {
      this.invalid = true;
      return;
    }
    
    if ( radiusX < radiusY ) {
      // TODO: check this
      throw new Error( 'Not verified to work if radiusX < radiusY' );
    }
    
    // compute an actual end angle so that we can smoothly go from this.startAngle to this.actualEndAngle
    if ( this.anticlockwise ) {
      // angle is 'decreasing'
      // -2pi <= end - start < 2pi
      if ( this.startAngle > this.endAngle ) {
        this.actualEndAngle = this.endAngle;
      } else if ( this.startAngle < this.endAngle ) {
        this.actualEndAngle = this.endAngle - 2 * Math.PI;
      } else {
        // equal
        this.actualEndAngle = this.startAngle;
      }
    } else {
      // angle is 'increasing'
      // -2pi < end - start <= 2pi
      if ( this.startAngle < this.endAngle ) {
        this.actualEndAngle = this.endAngle;
      } else if ( this.startAngle > this.endAngle ) {
        this.actualEndAngle = this.endAngle + Math.PI * 2;
      } else {
        // equal
        this.actualEndAngle = this.startAngle;
      }
    }
    
    // constraints shared with Segment.Arc
    assert && assert( !( ( !anticlockwise && endAngle - startAngle <= -Math.PI * 2 ) || ( anticlockwise && startAngle - endAngle <= -Math.PI * 2 ) ), 'Not handling elliptical arcs with start/end angles that show differences in-between browser handling' );
    assert && assert( !( ( !anticlockwise && endAngle - startAngle > Math.PI * 2 ) || ( anticlockwise && startAngle - endAngle > Math.PI * 2 ) ), 'Not handling elliptical arcs with start/end angles that show differences in-between browser handling' );
    
    var isFullPerimeter = ( !anticlockwise && endAngle - startAngle >= Math.PI * 2 ) || ( anticlockwise && startAngle - endAngle >= Math.PI * 2 );
    
    // compute an angle difference that represents how "much" of the circle our arc covers
    this.angleDifference = this.anticlockwise ? this.startAngle - this.endAngle : this.endAngle - this.startAngle;
    if ( this.angleDifference < 0 ) {
      this.angleDifference += Math.PI * 2;
    }
    assert && assert( this.angleDifference >= 0 ); // now it should always be zero or positive
    
    // a unit arg segment that we can map to our ellipse. useful for hit testing and such.
    this.unitArcSegment = new Segment.Arc( Vector2.ZERO, 1, startAngle, endAngle, anticlockwise );
    
    this.bounds = Bounds2.NOTHING;
    this.bounds = this.bounds.withPoint( this.start );
    this.bounds = this.bounds.withPoint( this.end );
    
    // for bounds computations
    var that = this;
    function boundsAtAngle( angle ) {
      if ( that.containsAngle( angle ) ) {
        // the boundary point is in the arc
        that.bounds = that.bounds.withPoint( that.positionAtAngle( angle ) );
      }
    }
    
    // if the angles are different, check extrema points
    if ( startAngle !== endAngle ) {
      // solve the mapping from the unit circle, find locations where a coordinate of the gradient is zero.
      // we find one extrema point for both x and y, since the other two are just rotated by pi from them.
      var xAngle = Math.atan( -( radiusY / radiusX ) * Math.tan( rotation ) );
      var yAngle = Math.atan( ( radiusY / radiusX ) / Math.tan( rotation ) );
      
      // check all of the extrema points
      this.possibleExtremaAngles = [
        xAngle,
        xAngle + Math.PI,
        yAngle,
        yAngle + Math.PI
      ];
      
      _.each( this.possibleExtremaAngles, boundsAtAngle );
    }
  };
  inherit( Segment, Segment.EllipticalArc, {
    
    // maps a contained angle to between [startAngle,actualEndAngle), even if the end angle is lower.
    mapAngle: function( angle ) {
      // consider an assert that we contain that angle?
      return ( this.startAngle > this.actualEndAngle ) ?
             DotUtil.moduloBetweenUp( angle, this.startAngle - 2 * Math.PI, this.startAngle ) :
             DotUtil.moduloBetweenDown( angle, this.startAngle, this.startAngle + 2 * Math.PI );
    },
    
    tAtAngle: function( angle ) {
      return ( this.mapAngle( angle ) - this.startAngle ) / ( this.actualEndAngle - this.startAngle );
    },
    
    angleAt: function( t ) {
      return this.startAngle + ( this.actualEndAngle - this.startAngle ) * t;
    },
    
    positionAt: function( t ) {
      return this.positionAtAngle( this.angleAt( t ) );
    },
    
    tangentAt: function( t ) {
      return this.tangentAtAngle( this.angleAt( t ) );
    },
    
    curvatureAt: function( t ) {
      // see http://mathworld.wolfram.com/Ellipse.html (59)
      var angle = this.angleAt( t );
      var aq = this.radiusX * Math.sin( angle );
      var bq = this.radiusY * Math.cos( angle );
      var denominator = Math.pow( bq * bq + aq * aq, 3/2 );
      return ( this.anticlockwise ? -1 : 1 ) * this.radiusX * this.radiusY / denominator;
    },
    
    positionAtAngle: function( angle ) {
      return this.unitTransform.transformPosition2( Vector2.createPolar( 1, angle ) );
    },
    
    tangentAtAngle: function( angle ) {
      var normal = this.unitTransform.transformNormal2( Vector2.createPolar( 1, angle ) );
      
      return this.anticlockwise ? normal.perpendicular() : normal.perpendicular().negated();
    },
    
    // TODO: refactor? exact same as Segment.Arc
    containsAngle: function( angle ) {
      // transform the angle into the appropriate coordinate form
      // TODO: check anticlockwise version!
      var normalizedAngle = this.anticlockwise ? angle - this.endAngle : angle - this.startAngle;
      
      // get the angle between 0 and 2pi
      var positiveMinAngle = normalizedAngle % ( Math.PI * 2 );
      // check this because modular arithmetic with negative numbers reveal a negative number
      if ( positiveMinAngle < 0 ) {
        positiveMinAngle += Math.PI * 2;
      }
      
      return positiveMinAngle <= this.angleDifference;
    },
    
    // discretizes the elliptical arc and returns an offset curve as a list of lineTos
    offsetTo: function( r, reverse ) {
      // how many segments to create (possibly make this more adaptive?)
      var quantity = 32;
      
      var points = [];
      var result = [];
      for ( var i = 0; i < quantity; i++ ) {
        var ratio = i / ( quantity - 1 );
        if ( reverse ) {
          ratio = 1 - ratio;
        }
        var angle = this.angleAt( ratio );
        
        points.push( this.positionAtAngle( angle ).plus( this.tangentAtAngle( angle ).perpendicular().normalized().times( r ) ) );
        if ( i > 0 ) {
          result.push( new Segment.Line( points[i-1], points[i] ) );
        }
      }
      
      return result;
    },
    
    getSVGPathFragment: function() {
      // see http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands for more info
      // rx ry x-axis-rotation large-arc-flag sweep-flag x y
      var epsilon = 0.01; // allow some leeway to render things as 'almost circles'
      var sweepFlag = this.anticlockwise ? '0' : '1';
      var largeArcFlag;
      var degreesRotation = toDegrees( this.rotation ); // bleh, degrees?
      if ( this.angleDifference < Math.PI * 2 - epsilon ) {
        largeArcFlag = this.angleDifference < Math.PI ? '0' : '1';
        return 'A ' + this.radiusX + ' ' + this.radiusY + ' ' + degreesRotation + ' ' + largeArcFlag + ' ' + sweepFlag + ' ' + this.end.x + ' ' + this.end.y;
      } else {
        // ellipse (or almost-ellipse) case needs to be handled differently
        // since SVG will not be able to draw (or know how to draw) the correct circle if we just have a start and end, we need to split it into two circular arcs
        
        // get the angle that is between and opposite of both of the points
        var splitOppositeAngle = ( this.startAngle + this.endAngle ) / 2; // this _should_ work for the modular case?
        var splitPoint = this.positionAtAngle( splitOppositeAngle );
        
        largeArcFlag = '0'; // since we split it in 2, it's always the small arc
        
        var firstArc = 'A ' + this.radiusX + ' ' + this.radiusY + ' ' + degreesRotation + ' ' + largeArcFlag + ' ' + sweepFlag + ' ' + splitPoint.x + ' ' + splitPoint.y;
        var secondArc = 'A ' + this.radiusX + ' ' + this.radiusY + ' ' + degreesRotation + ' ' + largeArcFlag + ' ' + sweepFlag + ' ' + this.end.x + ' ' + this.end.y;
        
        return firstArc + ' ' + secondArc;
      }
    },
    
    strokeLeft: function( lineWidth ) {
      return this.offsetTo( -lineWidth / 2, false );
    },
    
    strokeRight: function( lineWidth ) {
      return this.offsetTo( lineWidth / 2, true );
    },
    
    // not including 0 and 1
    getInteriorExtremaTs: function() {
      var that = this;
      var result = [];
      _.each( this.possibleExtremaAngles, function( angle ) {
        if ( that.containsAngle( angle ) ) {
          var t = that.tAtAngle( angle );
          var epsilon = 0.0000000001; // TODO: general kite epsilon?
          if ( t > epsilon && t < 1 - epsilon ) {
            result.push( t );
          }
        }
      } );
      return result.sort(); // modifies original, which is OK
    },
    
    subdivided: function( t ) {
      // TODO: verify that we don't need to switch anticlockwise here, or subtract 2pi off any angles
      var angle0 = this.angleAt( 0 );
      var angleT = this.angleAt( t );
      var angle1 = this.angleAt( 1 );
      return [
        new Segment.EllipticalArc( this.center, this.radiusX, this.radiusY, this.rotation, angle0, angleT, this.anticlockwise ),
        new Segment.EllipticalArc( this.center, this.radiusX, this.radiusY, this.rotation, angleT, angle1, this.anticlockwise )
      ];
    },
    
    intersectsBounds: function( bounds ) {
      throw new Error( 'Segment.EllipticalArc.intersectsBounds unimplemented' );
    },
    
    intersection: function( ray ) {
      // be lazy. transform it into the space of a non-elliptical arc.
      var unitTransform = this.unitTransform;
      var rayInUnitCircleSpace = unitTransform.inverseRay2( ray );
      var hits = this.unitArcSegment.intersection( rayInUnitCircleSpace );
      
      return _.map( hits, function( hit ) {
        var transformedPoint = unitTransform.transformPosition2( hit.point );
        return {
          distance: ray.pos.distance( transformedPoint ),
          point: transformedPoint,
          normal: unitTransform.inverseNormal2( hit.normal ),
          wind: hit.wind
        };
      } );
    },
    
    // returns the resultant winding number of this ray intersecting this segment.
    windingIntersection: function( ray ) {
      // be lazy. transform it into the space of a non-elliptical arc.
      var rayInUnitCircleSpace = this.unitTransform.inverseRay2( ray );
      return this.unitArcSegment.windingIntersection( rayInUnitCircleSpace );
    },
    
    // assumes the current position is at start
    writeToContext: function( context ) {
      if ( context.ellipse ) {
        context.ellipse( this.center.x, this.center.y, this.radiusX, this.radiusY, this.rotation, this.startAngle, this.endAngle, this.anticlockwise );
      } else {
        // fake the ellipse call by using transforms
        this.unitTransform.getMatrix().canvasAppendTransform( context );
        context.arc( 0, 0, 1, this.startAngle, this.endAngle, this.anticlockwise );
        this.unitTransform.getInverse().canvasAppendTransform( context );
      }
    },
    
    transformed: function( matrix ) {
      var transformedSemiMajorAxis = matrix.timesVector2( Vector2.createPolar( this.radiusX, this.rotation ) ).minus( matrix.timesVector2( Vector2.ZERO ) );
      var transformedSemiMinorAxis = matrix.timesVector2( Vector2.createPolar( this.radiusY, this.rotation + Math.PI / 2 ) ).minus( matrix.timesVector2( Vector2.ZERO ) );
      var rotation = transformedSemiMajorAxis.angle();
      var radiusX = transformedSemiMajorAxis.magnitude();
      var radiusY = transformedSemiMinorAxis.magnitude();
      
      var reflected = matrix.getDeterminant() < 0;
      
      // reverse the 'clockwiseness' if our transform includes a reflection
      // TODO: check reflections. swapping angle signs should fix clockwiseness
      var anticlockwise = reflected ? !this.anticlockwise : this.anticlockwise;
      var startAngle = reflected ? -this.startAngle : this.startAngle;
      var endAngle = reflected ? -this.endAngle : this.endAngle;
      
      return new Segment.EllipticalArc( matrix.timesVector2( this.center ), radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise );
    }
  } );
  
  // adapted from http://www.w3.org/TR/SVG/implnote.html#PathElementImplementationNotes
  // transforms the unit circle onto our ellipse
  Segment.EllipticalArc.computeUnitTransform = function( center, radiusX, radiusY, rotation ) {
    return new Transform3( Matrix3.translation( center.x, center.y ) // TODO: convert to Matrix3.translation( this.center) when available
                                  .timesMatrix( Matrix3.rotation2( rotation ) )
                                  .timesMatrix( Matrix3.scaling( radiusX, radiusY ) ) );
  };
  
  return Segment.EllipticalArc;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Shape handling
 *
 * Shapes are internally made up of Subpaths, which contain a series of segments, and are optionally closed.
 * Familiarity with how Canvas handles subpaths is helpful for understanding this code.
 *
 * Canvas spec: http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html
 * SVG spec: http://www.w3.org/TR/SVG/expanded-toc.html
 *           http://www.w3.org/TR/SVG/paths.html#PathData (for paths)
 * Notes for elliptical arcs: http://www.w3.org/TR/SVG/implnote.html#PathElementImplementationNotes
 * Notes for painting strokes: https://svgwg.org/svg2-draft/painting.html
 *
 * TODO: add nonzero / evenodd support when browsers support it
 * TODO: docs
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('KITE/Shape',['require','ASSERT/assert','ASSERT/assert','KITE/kite','DOT/Vector2','DOT/Bounds2','DOT/Ray2','DOT/Matrix3','DOT/Transform3','DOT/Util','DOT/Util','KITE/util/Subpath','KITE/../parser/svgPath','KITE/util/LineStyles','KITE/segments/Arc','KITE/segments/Cubic','KITE/segments/EllipticalArc','KITE/segments/Line','KITE/segments/Quadratic'], function( require ) {
  
  
  var assert = require( 'ASSERT/assert' )( 'kite' );
  var assertExtra = require( 'ASSERT/assert' )( 'kite.extra', true );
  
  var kite = require( 'KITE/kite' );
  
  // TODO: clean up imports
  var Vector2 = require( 'DOT/Vector2' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Ray2 = require( 'DOT/Ray2' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var Transform3 = require( 'DOT/Transform3' );
  var toDegrees = require( 'DOT/Util' ).toDegrees;
  var lineLineIntersection = require( 'DOT/Util' ).lineLineIntersection;
  
  var Subpath = require( 'KITE/util/Subpath' );
  
  var svgPath = require( 'KITE/../parser/svgPath' );
  require( 'KITE/util/LineStyles' );
  require( 'KITE/segments/Arc' );
  require( 'KITE/segments/Cubic' );
  require( 'KITE/segments/EllipticalArc' );
  require( 'KITE/segments/Line' );
  require( 'KITE/segments/Quadratic' );
  
  // for brevity
  function p( x,y ) { return new Vector2( x, y ); }
  function v( x,y ) { return new Vector2( x, y ); } // TODO: use this version in general, it makes more sense and is easier to type
  
  // a normalized vector for non-zero winding checks
  // var weirdDir = p( Math.PI, 22 / 7 );
  
  // all arguments optional, they are for the copy() method. if used, ensure that 'bounds' is consistent with 'subpaths'
  kite.Shape = function Shape( subpaths, bounds ) {
    // lower-level piecewise mathematical description using segments, also individually immutable
    this.subpaths = ( typeof subpaths === 'object' ) ? subpaths : [];
    assert && assert( this.subpaths.length === 0 || this.subpaths[0].constructor.name !== 'Array' );
    
    // computed bounds for all pieces added so far
    this.bounds = bounds || Bounds2.NOTHING;
    
    var that = this;
    if ( subpaths && typeof subpaths !== 'object' ) {
      assert && assert( typeof subpaths === 'string', 'if subpaths is not an object, it must be a string' );
      // parse the SVG path
      _.each( svgPath.parse( subpaths ), function( item ) {
        assert && assert( Shape.prototype[item.cmd] !== undefined, 'method ' + item.cmd + ' from parsed SVG does not exist' );
        that[item.cmd].apply( that, item.args );
      } );
    }
    
    phetAllocation && phetAllocation( 'Shape' );
  };
  var Shape = kite.Shape;
  
  Shape.prototype = {
    constructor: Shape,
    
    moveTo: function( x, y ) { return this.moveToPoint( v( x, y ) ); },
    moveToRelative: function( x, y ) { return this.moveToPointRelative( v( x, y ) ); },
    moveToPointRelative: function( point ) { return this.moveToPoint( this.getRelativePoint().plus( point ) ); },
    moveToPoint: function( point ) {
      return this.addSubpath( new kite.Subpath().addPoint( point ) );
    },
    
    lineTo: function( x, y ) { return this.lineToPoint( v( x, y ) ); },
    lineToRelative: function( x, y ) { return this.lineToPointRelative( v( x, y ) ); },
    lineToPointRelative: function( point ) { return this.lineToPoint( this.getRelativePoint().plus( point ) ); },
    lineToPoint: function( point ) {
      // see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-lineto
      if ( this.hasSubpaths() ) {
        var start = this.getLastSubpath().getLastPoint();
        var end = point;
      var line = new kite.Segment.Line( start, end );
        this.getLastSubpath().addPoint( end );
        if ( !line.invalid ) {
          this.getLastSubpath().addSegment( line );
          this.bounds = this.bounds.withPoint( start ).withPoint( end );
          assert && assert( !isNaN( this.bounds.getX() ) );
        }
      } else {
        this.ensure( point );
      }
      
      return this;
    },
    
    horizontalLineTo: function( x ) { return this.lineTo( x, this.getRelativePoint().y ); },
    horizontalLineToRelative: function( x ) { return this.lineToRelative( x, 0 ); },
    
    verticalLineTo: function( y ) { return this.lineTo( this.getRelativePoint().x, y ); },
    verticalLineToRelative: function( y ) { return this.lineToRelative( 0, y ); },
    
    quadraticCurveTo: function( cpx, cpy, x, y ) { return this.quadraticCurveToPoint( v( cpx, cpy ), v( x, y ) ); },
    quadraticCurveToRelative: function( cpx, cpy, x, y ) { return this.quadraticCurveToPointRelative( v( cpx, cpy ), v( x, y ) ); },
    quadraticCurveToPointRelative: function( controlPoint, point ) {
      var relativePoint = this.getRelativePoint();
      return this.quadraticCurveToPoint( relativePoint.plus( controlPoint ), relativePoint.plus( point ) );
    },
    // TODO: consider a rename to put 'smooth' farther back?
    smoothQuadraticCurveTo: function( x, y ) { return this.quadraticCurveToPoint( this.getSmoothQuadraticControlPoint(), v( x, y ) ); },
    smoothQuadraticCurveToRelative: function( x, y ) { return this.quadraticCurveToPoint( this.getSmoothQuadraticControlPoint(), v( x, y ).plus( this.getRelativePoint() ) ); },
    quadraticCurveToPoint: function( controlPoint, point ) {
      // see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-quadraticcurveto
      this.ensure( controlPoint );
      var start = this.getLastSubpath().getLastPoint();
      var quadratic = new kite.Segment.Quadratic( start, controlPoint, point );
      this.getLastSubpath().addPoint( point );
      if ( !quadratic.invalid ) {
        this.getLastSubpath().addSegment( quadratic );
        this.bounds = this.bounds.union( quadratic.bounds );
      }
      
      return this;
    },
    
    cubicCurveTo: function( cp1x, cp1y, cp2x, cp2y, x, y ) { return this.cubicCurveToPoint( v( cp1x, cp1y ), v( cp2x, cp2y ), v( x, y ) ); },
    cubicCurveToRelative: function( cp1x, cp1y, cp2x, cp2y, x, y ) { return this.cubicCurveToPointRelative( v( cp1x, cp1y ), v( cp2x, cp2y ), v( x, y ) ); },
    cubicCurveToPointRelative: function( control1, control2, point ) {
      var relativePoint = this.getRelativePoint();
      return this.cubicCurveToPoint( relativePoint.plus( control1 ), relativePoint.plus( control2 ), relativePoint.plus( point ) );
    },
    smoothCubicCurveTo: function( cp2x, cp2y, x, y ) { return this.cubicCurveToPoint( this.getSmoothCubicControlPoint(), v( cp2x, cp2y ), v( x, y ) ); },
    smoothCubicCurveToRelative: function( cp2x, cp2y, x, y ) { return this.cubicCurveToPoint( this.getSmoothCubicControlPoint(), v( cp2x, cp2y ).plus( this.getRelativePoint() ), v( x, y ).plus( this.getRelativePoint() ) ); },
    cubicCurveToPoint: function( control1, control2, point ) {
      // see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-quadraticcurveto
      this.ensure( control1 );
      var start = this.getLastSubpath().getLastPoint();
      var cubic = new kite.Segment.Cubic( start, control1, control2, point );
      
      if ( !cubic.invalid ) {
        // if there is a cusp, we add the two (split) quadratic segments instead so that stroking treats the 'join' between them with the proper lineJoin
        if ( cubic.hasCusp() ) {
          this.getLastSubpath().addSegment( cubic.startQuadratic );
          this.getLastSubpath().addSegment( cubic.endQuadratic );
        } else {
          this.getLastSubpath().addSegment( cubic );
        }
        
        this.bounds = this.bounds.union( cubic.bounds );
      }
      this.getLastSubpath().addPoint( point );
      
      return this;
    },
    
    arc: function( centerX, centerY, radius, startAngle, endAngle, anticlockwise ) { return this.arcPoint( v( centerX, centerY ), radius, startAngle, endAngle, anticlockwise ); },
    arcPoint: function( center, radius, startAngle, endAngle, anticlockwise ) {
      // see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-arc
      
      var arc = new kite.Segment.Arc( center, radius, startAngle, endAngle, anticlockwise );
      
      // we are assuming that the normal conditions were already met (or exceptioned out) so that these actually work with canvas
      var startPoint = arc.start;
      var endPoint = arc.end;
      
      // if there is already a point on the subpath, and it is different than our starting point, draw a line between them
      if ( this.hasSubpaths() && this.getLastSubpath().getLength() > 0 && !startPoint.equals( this.getLastSubpath().getLastPoint(), 0 ) ) {
        this.getLastSubpath().addSegment( new kite.Segment.Line( this.getLastSubpath().getLastPoint(), startPoint ) );
      }
      
      if ( !this.hasSubpaths() ) {
        this.addSubpath( new kite.Subpath() );
      }
      
      // technically the Canvas spec says to add the start point, so we do this even though it is probably completely unnecessary (there is no conditional)
      this.getLastSubpath().addPoint( startPoint );
      this.getLastSubpath().addPoint( endPoint );
      
      if ( !arc.invalid ) {
        this.getLastSubpath().addSegment( arc );
        
        // and update the bounds
        this.bounds = this.bounds.union( arc.bounds );
      }
      
      return this;
    },
    
    ellipticalArc: function( centerX, centerY, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise ) { return this.ellipticalArcPoint( v( centerX, centerY ), radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise ); },
    ellipticalArcPoint: function( center, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise ) {
      // see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-arc
      
      var ellipticalArc = new kite.Segment.EllipticalArc( center, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise );
      
      // we are assuming that the normal conditions were already met (or exceptioned out) so that these actually work with canvas
      var startPoint = ellipticalArc.start;
      var endPoint = ellipticalArc.end;
      
      // if there is already a point on the subpath, and it is different than our starting point, draw a line between them
      if ( this.hasSubpaths() && this.getLastSubpath().getLength() > 0 && !startPoint.equals( this.getLastSubpath().getLastPoint(), 0 ) ) {
        this.getLastSubpath().addSegment( new kite.Segment.Line( this.getLastSubpath().getLastPoint(), startPoint ) );
      }
      
      if ( !this.hasSubpaths() ) {
        this.addSubpath( new kite.Subpath() );
      }
      
      // technically the Canvas spec says to add the start point, so we do this even though it is probably completely unnecessary (there is no conditional)
      this.getLastSubpath().addPoint( startPoint );
      this.getLastSubpath().addPoint( endPoint );
      
      if ( !ellipticalArc.invalid ) {
        this.getLastSubpath().addSegment( ellipticalArc );
        
        // and update the bounds
        this.bounds = this.bounds.union( ellipticalArc.bounds );
      }
      
      return this;
    },
    
    close: function() {
      if ( this.hasSubpaths() ) {
        var previousPath = this.getLastSubpath();
        var nextPath = new kite.Subpath();
        
        previousPath.close();
        this.addSubpath( nextPath );
        nextPath.addPoint( previousPath.getFirstPoint() );
      }
      return this;
    },
    
    // matches SVG's elliptical arc from http://www.w3.org/TR/SVG/paths.html
    ellipticalArcToRelative: function( radiusX, radiusY, rotation, largeArc, sweep, x, y ) {
      var relativePoint = this.getRelativePoint();
      return this.ellipticalArcTo( radiusX, radiusY, rotation, largeArc, sweep, x + relativePoint.x, y + relativePoint.y );
    },
    ellipticalArcTo: function( radiusX, radiusY, rotation, largeArc, sweep, x, y ) {
      throw new Error( 'ellipticalArcTo unimplemented' );
    },
    
    /*
     * Draws a circle using the arc() call with the following parameters:
     * circle( center, radius ) // center is a Vector2
     * circle( centerX, centerY, radius )
     */
    circle: function( centerX, centerY, radius ) {
      if ( typeof centerX === 'object' ) {
        // circle( center, radius )
        var center = centerX;
        radius = centerY;
        return this.arcPoint( center, radius, 0, Math.PI * 2, false );
      } else {
        // circle( centerX, centerY, radius )
        return this.arcPoint( p( centerX, centerY ), radius, 0, Math.PI * 2, false );
      }
    },
    
    /*
     * Draws an ellipse using the ellipticalArc() call with the following parameters:
     * ellipse( center, radiusX, radiusY, rotation ) // center is a Vector2
     * ellipse( centerX, centerY, radiusX, radiusY, rotation )
     */
    ellipse: function( centerX, centerY, radiusX, radiusY, rotation ) {
      // TODO: separate into ellipse() and ellipsePoint()?
      // TODO: Ellipse/EllipticalArc has a mess of parameters. Consider parameter object, or double-check parameter handling
      if ( typeof centerX === 'object' ) {
        // ellipse( center, radiusX, radiusY, rotation )
        var center = centerX;
        rotation = radiusY;
        radiusY = radiusX;
        radiusX = centerY;
        return this.ellipticalArcPoint( center, radiusX, radiusY, rotation || 0, 0, Math.PI * 2, false );
      } else {
        // ellipse( centerX, centerY, radiusX, radiusY, rotation )
        return this.ellipticalArcPoint( v( centerX, centerY ), radiusX, radiusY, rotation || 0, 0, Math.PI * 2, false );
      }
    },
    
    rect: function( x, y, width, height ) {
      var subpath = new kite.Subpath();
      this.addSubpath( subpath );
      subpath.addPoint( v( x, y ) );
      subpath.addPoint( v( x + width, y ) );
      subpath.addPoint( v( x + width, y + height ) );
      subpath.addPoint( v( x, y + height ) );
      subpath.addSegment( new kite.Segment.Line( subpath.points[0], subpath.points[1] ) );
      subpath.addSegment( new kite.Segment.Line( subpath.points[1], subpath.points[2] ) );
      subpath.addSegment( new kite.Segment.Line( subpath.points[2], subpath.points[3] ) );
      subpath.close();
      this.addSubpath( new kite.Subpath() );
      this.getLastSubpath().addPoint( v( x, y ) );
      this.bounds = this.bounds.withCoordinates( x, y ).withCoordinates( x + width, y + height );
      assert && assert( !isNaN( this.bounds.getX() ) );
      
      return this;
    },

    //Create a round rectangle. All arguments are number.
    roundRect: function( x, y, width, height, arcw, arch ) {
      var lowX = x + arcw;
      var highX = x + width - arcw;
      var lowY = y + arch;
      var highY = y + height - arch;
      // if ( true ) {
      if ( arcw === arch ) {
        // we can use circular arcs, which have well defined stroked offsets
        this.arc( highX, lowY, arcw, -Math.PI / 2, 0, false )
            .arc( highX, highY, arcw, 0, Math.PI / 2, false )
            .arc( lowX, highY, arcw, Math.PI / 2, Math.PI, false )
            .arc( lowX, lowY, arcw, Math.PI, Math.PI * 3 / 2, false )
            .close();
      } else {
        // we have to resort to elliptical arcs
        this.ellipticalArc( highX, lowY, arcw, arch, 0, -Math.PI / 2, 0, false )
            .ellipticalArc( highX, highY, arcw, arch, 0, 0, Math.PI / 2, false )
            .ellipticalArc( lowX, highY, arcw, arch, 0, Math.PI / 2, Math.PI, false )
            .ellipticalArc( lowX, lowY, arcw, arch, 0, Math.PI, Math.PI * 3 / 2, false )
            .close();
      }
      return this;
    },
    
    polygon: function( vertices ) {
      var length = vertices.length;
      if ( length > 0 ) {
        this.moveToPoint( vertices[0] );
        for ( var i = 1; i < length; i++ ) {
          this.lineToPoint( vertices[i] );
        }
      }
      return this.close();
    },
    
    copy: function() {
      // copy each individual subpath, so future modifications to either Shape doesn't affect the other one
      return new Shape( _.map( this.subpaths, function( subpath ) { return subpath.copy(); } ), this.bounds );
    },
    
    // write out this shape's path to a canvas 2d context. does NOT include the beginPath()!
    writeToContext: function( context ) {
      var len = this.subpaths.length;
      for ( var i = 0; i < len; i++ ) {
        this.subpaths[i].writeToContext( context );
      }
    },
    
    // returns something like "M150 0 L75 200 L225 200 Z" for a triangle
    getSVGPath: function() {
      var subpathStrings = [];
      var len = this.subpaths.length;
      for ( var i = 0; i < len; i++ ) {
        var subpath = this.subpaths[i];
        if( subpath.isDrawable() ) {
          // since the commands after this are relative to the previous 'point', we need to specify a move to the initial point
          var startPoint = subpath.getFirstSegment().start;
          assert && assert( startPoint.equals( subpath.getFirstPoint(), 0.00001 ) ); // sanity check
          var string = 'M ' + startPoint.x + ' ' + startPoint.y + ' ';
          
          string += _.map( subpath.segments, function( segment ) { return segment.getSVGPathFragment(); } ).join( ' ' );
          
          if ( subpath.isClosed() ) {
            string += ' Z';
          }
          subpathStrings.push( string );
        }
      }
      return subpathStrings.join( ' ' );
    },
    
    // return a new Shape that is transformed by the associated matrix
    transformed: function( matrix ) {
      var subpaths = _.map( this.subpaths, function( subpath ) { return subpath.transformed( matrix ); } );
      var bounds = _.reduce( subpaths, function( bounds, subpath ) { return bounds.union( subpath.computeBounds() ); }, Bounds2.NOTHING );
      return new Shape( subpaths, bounds );
    },
    
    // returns the bounds. if lineStyles exists, include the stroke in the bounds
    // TODO: consider renaming to getBounds()?
    computeBounds: function( lineStyles ) {
      if ( lineStyles ) {
        return this.bounds.union( this.getStrokedShape( lineStyles ).bounds );
      } else {
        return this.bounds;
      }
    },
    
    containsPoint: function( point ) {
      // we pick a ray, and determine the winding number over that ray. if the number of segments crossing it CCW == number of segments crossing it CW, then the point is contained in the shape
      var ray = new Ray2( point, Vector2.X_UNIT );
      
      return this.windingIntersection( ray ) !== 0;
    },
    
    intersection: function( ray ) {
      var hits = [];
      var numSubpaths = this.subpaths.length;
      for ( var i = 0; i < numSubpaths; i++ ) {
        var subpath = this.subpaths[i];
        
        if ( subpath.isDrawable() ) {
          var numSegments = subpath.segments.length;
          for ( var k = 0; k < numSegments; k++ ) {
            var segment = subpath.segments[k];
            hits = hits.concat( segment.intersection( ray ) );
          }
          
          if ( subpath.hasClosingSegment() ) {
            hits = hits.concat( subpath.getClosingSegment().intersection( ray ) );
          }
        }
      }
      return _.sortBy( hits, function( hit ) { return hit.distance; } );
    },
    
    windingIntersection: function( ray ) {
      var wind = 0;
      
      var numSubpaths = this.subpaths.length;
      for ( var i = 0; i < numSubpaths; i++ ) {
        var subpath = this.subpaths[i];
        
        if ( subpath.isDrawable() ) {
          var numSegments = subpath.segments.length;
          for ( var k = 0; k < numSegments; k++ ) {
            wind += subpath.segments[k].windingIntersection( ray );
          }
          
          // handle the implicit closing line segment
          if ( subpath.hasClosingSegment() ) {
            wind += subpath.getClosingSegment().windingIntersection( ray );
          }
        }
      }
      
      return wind;
    },
    
    intersectsBounds: function( bounds ) {
      var numSubpaths = this.subpaths.length;
      for ( var i = 0; i < numSubpaths; i++ ) {
        var subpath = this.subpaths[i];
        
        if ( subpath.isDrawable() ) {
          var numSegments = subpath.segments.length;
          for ( var k = 0; k < numSegments; k++ ) {
            if ( subpath.segments[k].intersectsBounds( bounds ) ) {
              return true;
            }
          }
          
          // handle the implicit closing line segment
          if ( subpath.hasClosingSegment() ) {
            if ( subpath.getClosingSegment().intersectsBounds( bounds ) ) {
              return true;
            }
          }
        }
      }
      return false;
    },
    
    // returns a new Shape that is an outline of the stroked path of this current Shape. currently not intended to be nested (doesn't do intersection computations yet)
    // TODO: rename stroked( lineStyles )
    getStrokedShape: function( lineStyles ) {
      var subpaths = [];
      var bounds = Bounds2.NOTHING.copy();
      var subLen = this.subpaths.length;
      for ( var i = 0; i < subLen; i++ ) {
        var subpath = this.subpaths[i];
        var strokedSubpath = subpath.stroked( lineStyles );
        subpaths = subpaths.concat( strokedSubpath );
      }
      subLen = subpaths.length;
      for ( i = 0; i < subLen; i++ ) {
        bounds.includeBounds( subpaths[i].computeBounds() );
      }
      return new Shape( subpaths, bounds );
    },
    
    toString: function() {
      // TODO: consider a more verbose but safer way?
      return 'new kite.Shape( \'' + this.getSVGPath() + '\' )';
    },
    
    /*---------------------------------------------------------------------------*
    * Internal subpath computations
    *----------------------------------------------------------------------------*/
    
    ensure: function( point ) {
      if ( !this.hasSubpaths() ) {
        this.addSubpath( new Subpath() );
        this.getLastSubpath().addPoint( point );
      }
    },
    
    addSubpath: function( subpath ) {
      this.subpaths.push( subpath );
      
      return this; // allow chaining
    },
    
    hasSubpaths: function() {
      return this.subpaths.length > 0;
    },
    
    getLastSubpath: function() {
      return _.last( this.subpaths );
    },
    
    // gets the last point in the last subpath, or null if it doesn't exist
    getLastPoint: function() {
      return this.hasSubpaths() ? this.getLastSubpath().getLastPoint() : null;
    },
    
    getLastSegment: function() {
      if ( !this.hasSubpaths() ) { return null; }
      
      var subpath = this.getLastSubpath();
      if ( !subpath.isDrawable() ) { return null; }
      
      return subpath.getLastSegment();
    },
    
    // returns the point to be used for smooth quadratic segments
    getSmoothQuadraticControlPoint: function() {
      var lastPoint = this.getLastPoint();
      
      var segment = this.getLastSegment();
      if ( !segment || !( segment instanceof kite.Segment.Quadratic ) ) { return lastPoint; }
      
      return lastPoint.plus( lastPoint.minus( segment.control ) );
    },
    
    // returns the point to be used for smooth cubic segments
    getSmoothCubicControlPoint: function() {
      var lastPoint = this.getLastPoint();
      
      var segment = this.getLastSegment();
      if ( !segment || !( segment instanceof kite.Segment.Cubic ) ) { return lastPoint; }
      
      return lastPoint.plus( lastPoint.minus( segment.control2 ) );
    },
    
    getRelativePoint: function() {
      var lastPoint = this.getLastPoint();
      return lastPoint ? lastPoint : Vector2.ZERO;
    }
  };
  
  /*---------------------------------------------------------------------------*
  * Shape shortcuts
  *----------------------------------------------------------------------------*/
  
  Shape.rectangle = function( x, y, width, height ) {
    return new Shape().rect( x, y, width, height );
  };
  Shape.rect = Shape.rectangle;

  //Create a round rectangle. All arguments are number.
  //Rounding is currently using quadraticCurveTo.  Please note, future versions may use arcTo
  //TODO: rewrite with arcTo?
  Shape.roundRect = function( x, y, width, height, arcw, arch ) {
    return new Shape().roundRect( x, y, width, height, arcw, arch );
  };
  Shape.roundRectangle = Shape.roundRect;
  
  Shape.polygon = function( vertices ) {
    return new Shape().polygon( vertices );
  };
  
  Shape.bounds = function( bounds ) {
    return new Shape().rect( bounds.minX, bounds.minY, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY );
  };

  //Create a line segment, using either (x1,y1,x2,y2) or ({x1,y1},{x2,y2}) arguments
  Shape.lineSegment = function( a, b, c, d ) {
    // TODO: add type assertions?
    if ( typeof a === 'number' ) {
      return new Shape().moveTo( a, b ).lineTo( c, d );
    }
    else {
      return new Shape().moveToPoint( a ).lineToPoint( b );
    }
  };
  
  Shape.regularPolygon = function( sides, radius ) {
    var shape = new Shape();
    _.each( _.range( sides ), function( k ) {
      var point = Vector2.createPolar( radius, 2 * Math.PI * k / sides );
      ( k === 0 ) ? shape.moveToPoint( point ) : shape.lineToPoint( point );
    } );
    return shape.close();
  };
  
  // supports both circle( centerX, centerY, radius ), circle( center, radius ), and circle( radius ) with the center default to 0,0
  Shape.circle = function( centerX, centerY, radius ) {
    if ( centerY === undefined ) {
      // circle( radius ), center = 0,0
      return new Shape().circle( 0, 0, centerX );
    }
    return new Shape().circle( centerX, centerY, radius ).close();
  };
  
  /*
   * Supports ellipse( centerX, centerY, radiusX, radiusY ), ellipse( center, radiusX, radiusY ), and ellipse( radiusX, radiusY )
   * with the center default to 0,0 and rotation of 0
   */
  Shape.ellipse = function( centerX, centerY, radiusX, radiusY ) {
    // TODO: Ellipse/EllipticalArc has a mess of parameters. Consider parameter object, or double-check parameter handling
    if ( radiusX === undefined ) {
      // ellipse( radiusX, radiusY ), center = 0,0
      return new Shape().ellipse( 0, 0, centerX, centerY );
    }
    return new Shape().ellipse( centerX, centerY, radiusX, radiusY ).close();
  };
  
  // supports both arc( centerX, centerY, radius, startAngle, endAngle, anticlockwise ) and arc( center, radius, startAngle, endAngle, anticlockwise )
  Shape.arc = function( centerX, centerY, radius, startAngle, endAngle, anticlockwise ) {
    return new Shape().arc( centerX, centerY, radius, startAngle, endAngle, anticlockwise );
  };
  
  return Shape;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Mix-in for Node's event handling, with experimental performance enhancements
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/FixedNodeEvents',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  var eventNames = [
    'selfBounds',
    'childBounds',
    'bounds',
    'resize',
    'boundsAccuracy'
  ];
  
  scenery.FixedNodeEvents = function FixedNodeEvents( type ) {
    var proto = type.prototype;
    
    // this should be called in the constructor to initialize
    proto.initializeNodeEvents = function() {
      this._events = {};
      
      var node = this;
      
      // TODO: performance: consider delaying this? Could affect memory usage?
      var len = eventNames.length;
      for ( var i = 0; i < len; i++ ) {
        node._events[eventNames[i]] = [];
      }
    };
    
    /**
     * @param {String}   type     The type of event, like 'resize' or 'bounds'
     * @param {Function} listener Callback, called with arguments that depend on the event type
     */
    proto.addEventListener = function( type, listener ) {
      sceneryAssert && sceneryAssert( type !== undefined && listener !== undefined,
                                      'Both a type and listener are required for addEventListener' );
      
      // most commonly a bug, maybe there will be a good use case? can always work around by wrapping with a new function each time
      sceneryAssert && sceneryAssert( _.indexOf( this._events[type], listener ),
                                      'Event listener was already there for addEventListener with type ' + type );
      
      this._events[type].push( listener );
      
      // allow chaining
      return this;
    };
    
    /**
     * @param {String}   type     The type of event, like 'resize' or 'bounds'
     * @param {Function} listener The callback to remove.
     */
    proto.removeEventListener = function( type, listener ) {
      sceneryAssert && sceneryAssert( type !== undefined && listener !== undefined,
                                      'Both a type and listener are required for removeEventListener' );
      
      // ensure the listener is in our list
      sceneryAssert && sceneryAssert( _.indexOf( this._events[type], listener ) !== -1,
                                      'Listener did not exist for type ' + type );
      
      this._events[type].splice( _.indexOf( this._events[type], listener ), 1 );
      
      // allow chaining
      return this;
    };
    
    /*
     * Fires an event to all event listeners attached to this node. It does not bubble down to
     * all ancestors with trails, like dispatchEvent does. Use fireEvent when you only want an event
     * that is relevant for a specific node, and ancestors don't need to be notified.
     */
    proto.fireEvent = function( type, args ) {
      sceneryAssert && sceneryAssert( _.contains( eventNames, type ),
                                      'unknown event type: ' + type );
      
      var events = this._events[type];
      var len = events.length;
      if ( len ) { // TODO: consider removing branch? is this even helpful?
        var copy = events.slice( 0 ); // defensive copy, in case listeners are added or removed as a side effect of a listener being called
        for ( var i = 0; i < len; i++ ) {
          copy[i]( args );
        }
      }
    };
  };
  var FixedNodeEvents = scenery.FixedNodeEvents;
  
  return FixedNodeEvents;
} );



// Copyright 2002-2013, University of Colorado

/**
 * Controls the underlying layer behavior around a node. The node's LayerStrategy's enter() and exit() will be
 * called in a depth-first order during the layer building process, and will modify a LayerBuilder to signal any
 * layer-specific signals.
 *
 * This generally ensures that a layer containing the proper renderer and settings to support its associated node
 * will be created.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/LayerStrategy',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  /*
   * If the node specifies a renderer, we will always push a preferred type. That type will be fresh (if rendererOptions are specified), otherwise
   * the top matching preferred type for that renderer will be used. This allows us to always pop in the exit().
   *
   * Specified as such, since there is no needed shared state (we can have node.layerStrategy = scenery.LayerStrategy for many nodes)
   */
  scenery.LayerStrategy = {
    // true iff enter/exit will push/pop a layer type to the preferred stack. currently limited to only one layer type per level.
    hasPreferredLayerType: function( pointer, layerBuilder ) {
      return pointer.trail.lastNode().hasRenderer();
    },
    
    getPreferredLayerType: function( pointer, layerBuilder ) {
      sceneryAssert && sceneryAssert( this.hasPreferredLayerType( pointer, layerBuilder ) ); // sanity check
      
      var node = pointer.trail.lastNode();
      var preferredLayerType;
      
      if ( node.hasRendererLayerType() ) {
        preferredLayerType = node.getRendererLayerType();
      } else {
        preferredLayerType = layerBuilder.bestPreferredLayerTypeFor( [ node.getRenderer() ] );
        if ( !preferredLayerType ) {
          // there was no preferred layer type matching, just use the default
          preferredLayerType = node.getRenderer().defaultLayerType;
        }
      }
      
      return preferredLayerType;
    },
    
    enter: function( pointer, layerBuilder ) {
      var trail = pointer.trail;
      var node = trail.lastNode();
      var preferredLayerType;
      
      // if the node has a renderer, always push a layer type, so that we can pop on the exit() and ensure consistent behavior
      if ( node.hasRenderer() ) {
        preferredLayerType = this.getPreferredLayerType( pointer, layerBuilder );
        
        // push the preferred layer type
        layerBuilder.pushPreferredLayerType( preferredLayerType );
        if ( layerBuilder.getCurrentLayerType() !== preferredLayerType ) {
          layerBuilder.switchToType( pointer, preferredLayerType );
        }
      } else if ( node.isPainted() ) {
        // node doesn't specify a renderer, but isPainted.
        
        var supportedRenderers = node._supportedRenderers;
        var currentType = layerBuilder.getCurrentLayerType();
        preferredLayerType = layerBuilder.bestPreferredLayerTypeFor( supportedRenderers );
        
        // If any of the preferred types are compatible, use the top one. This allows us to support caching and hierarchical layer types
        if ( preferredLayerType ) {
          if ( currentType !== preferredLayerType ) {
            layerBuilder.switchToType( pointer, preferredLayerType );
          }
        } else {
          // if no preferred types are compatible, only switch if the current type is also incompatible
          if ( !currentType || !currentType.supportsNode( node ) ) {
            layerBuilder.switchToType( pointer, supportedRenderers[0].defaultLayerType );
          }
        }
      }
      
      if ( node.isLayerSplitBefore() || this.hasSplitFlags( node ) ) {
        layerBuilder.switchToType( pointer, layerBuilder.getCurrentLayerType() );
      }
      
      if ( node.isPainted() ) {
        // trigger actual layer creation if necessary (allow collapsing of layers otherwise)
        layerBuilder.markPainted( pointer );
      }
    },
    
    // afterSelf: function( trail, layerBuilder ) {
    //   // no-op, and possibly not used
    // },
    
    // betweenChildren: function( trail, layerBuilder ) {
    //   // no-op, and possibly not used
    // },
    
    exit: function( pointer, layerBuilder ) {
      var trail = pointer.trail;
      var node = trail.lastNode();
      
      if ( node.hasRenderer() ) {
        layerBuilder.popPreferredLayerType();
        
        // switch down to the next lowest preferred layer type, if any. if null, pass the null to switchToType
        // this allows us to not 'leak' the renderer information, and the temporary layer type is most likely collapsed and ignored
        // NOTE: disabled for now, since this prevents us from having adjacent children sharing the same layer type
        // if ( layerBuilder.getCurrentLayerType() !== layerBuilder.getPreferredLayerType() ) {
        //   layerBuilder.switchToType( pointer, layerBuilder.getPreferredLayerType() );
        // }
      }
      
      if ( node.isLayerSplitAfter() || this.hasSplitFlags( node ) ) {
        layerBuilder.switchToType( pointer, layerBuilder.getCurrentLayerType() );
      }
    },
    
    // whether splitting before and after the node is required
    hasSplitFlags: function( node ) {
      // currently, only enforce splitting if we are using CSS transforms
      var rendererOptions = node.getRendererOptions();
      return node.hasRenderer() && rendererOptions && (
        rendererOptions.cssTranslation ||
        rendererOptions.cssRotation ||
        rendererOptions.cssScale ||
        rendererOptions.cssTransform
      );
    }
  };
  var LayerStrategy = scenery.LayerStrategy;
  
  return LayerStrategy;
} );

// Copyright 2002-2013, University of Colorado

/**
 * A node for the Scenery scene graph. Supports general directed acyclic graphics (DAGs).
 * Handles multiple layers with assorted types (Canvas 2D, SVG, DOM, WebGL, etc.).
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/Node',['require','DOT/Bounds2','DOT/Transform3','DOT/Matrix3','DOT/Vector2','DOT/Util','KITE/Shape','SCENERY/scenery','SCENERY/util/FixedNodeEvents','SCENERY/layers/LayerStrategy'], function( require ) {
  
  
  var Bounds2 = require( 'DOT/Bounds2' );
  var Transform3 = require( 'DOT/Transform3' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var Vector2 = require( 'DOT/Vector2' );
  var clamp = require( 'DOT/Util' ).clamp;
  
  var Shape = require( 'KITE/Shape' );
  
  var scenery = require( 'SCENERY/scenery' );
  var NodeEvents = require( 'SCENERY/util/FixedNodeEvents' ); // uncapitalized, because of JSHint (TODO: find the flag)
  var LayerStrategy = require( 'SCENERY/layers/LayerStrategy' ); // used to set the default layer strategy on the prototype
  // require( 'SCENERY/layers/Renderer' ); // commented out so Require.js doesn't balk at the circular dependency
  
  // TODO: FIXME: Why do I have to comment out this dependency?
  // require( 'SCENERY/util/Trail' );
  // require( 'SCENERY/util/TrailPointer' );
  
  var globalIdCounter = 1;
  
  /*
   * Available keys for use in the options parameter object for a vanilla Node (not inherited), in the order they are executed in:
   *
   * children:         A list of children to add (in order)
   * cursor:           Will display the specified CSS cursor when the mouse is over this Node or one of its descendents. The Scene needs to have input listeners attached with an initialize method first.
   * visible:          If false, this node (and its children) will not be displayed (or get input events)
   * pickable:         If false, this node (and its children) will not get input events
   * translation:      Sets the translation of the node to either the specified dot.Vector2 value, or the x,y values from an object (e.g. translation: { x: 1, y: 2 } )
   * x:                Sets the x-translation of the node
   * y:                Sets the y-translation of the node
   * rotation:         Sets the rotation of the node in radians
   * scale:            Sets the scale of the node. Supports either a number (same x-y scale), or a dot.Vector2 / object with ob.x and ob.y to set the scale for each axis independently
   * left:             Sets the x-translation so that the left (min X) of the bounding box (in the parent coordinate frame) is at the specified value
   * right:            Sets the x-translation so that the right (max X) of the bounding box (in the parent coordinate frame) is at the specified value
   * top:              Sets the y-translation so that the top (min Y) of the bounding box (in the parent coordinate frame) is at the specified value
   * bottom:           Sets the y-translation so that the bottom (min Y) of the bounding box (in the parent coordinate frame) is at the specified value
   * centerX:          Sets the x-translation so that the horizontal center of the bounding box (in the parent coordinate frame) is at the specified value
   * centerY:          Sets the y-translation so that the vertical center of the bounding box (in the parent coordinate frame) is at the specified value
   * renderer:         Forces Scenery to use the specific renderer (canvas/svg) to display this node (and if possible, children). Accepts both strings (e.g. 'canvas', 'svg', etc.) or actual Renderer objects (e.g. Renderer.Canvas, Renderer.SVG, etc.)
   * rendererOptions:  Parameter object that is passed to the created layer, and can affect how the layering process works.
   * layerSplit:       Forces a split between layers before and after this node (and its children) have been rendered. Useful for performance with Canvas-based renderers.
   * layerSplitBefore: Forces a split between layers before this node (and its children) have been rendered. Useful for performance with Canvas-based renderers.
   * layerSplitAfter:  Forces a split between layers after this node (and its children) have been rendered. Useful for performance with Canvas-based renderers.
   */
  scenery.Node = function Node( options ) {
    var self = this;
    
    // assign a unique ID to this node (allows trails to get a unique list of IDs)
    this._id = globalIdCounter++;
    
    // all of the Instances tracking this Node (across multiple layers and scenes)
    this._instances = [];
    
    // Whether this node (and its children) will be visible when the scene is updated. Visible nodes by default will not be pickable either
    this._visible = true;
    
    // Opacity from 0 to 1
    this._opacity = 1;
    
    // Whether hit testing will check for this node (and its children).
    this._pickable = true;
    
    // This node and all children will be clipped by this shape (in addition to any other clipping shapes).
    // The shape should be in the local coordinate frame
    this._clipShape = null;
    
    // areas for hit intersection. if set on a Node, no descendants can handle events
    this._mouseArea = null; // {Shape} for mouse position          in the local coordinate frame
    this._touchArea = null; // {Shape} for touch and pen position  in the local coordinate frame
    
    // the CSS cursor to be displayed over this node. null should be the default (inherit) value
    this._cursor = null;
    
    this._children = []; // ordered
    this._parents = []; // unordered
    
    this._peers = []; // array of peer factories: { element: ..., options: ... }, where element can be an element or a string
    this._liveRegions = []; // array of live region instances
    
    /*
     * Set up the transform reference. we add a listener so that the transform itself can be modified directly
     * by reference, or node.transform = <transform> / node.setTransform() can be used to change the transform reference.
     * Both should trigger the necessary event notifications for Scenery to keep track internally.
     */
    this._transform = new Transform3();
    this._transformListener = {
      // TODO: performance handling so we don't need to do two recursions!
      before: function() { self.beforeTransformChange(); },
      after: function() { self.afterTransformChange(); }
    };
    this._transform.addTransformListener( this._transformListener );
    
    this._inputListeners = []; // for user input handling (mouse/touch)
    this.initializeNodeEvents(); // for internal events like paint invalidation, layer invalidation, etc.
    
    // TODO: add getter/setters that will be able to invalidate whether this node is under any pointers, etc.
    this._includeStrokeInHitRegion = false;
    
    // bounds handling
    this._bounds = Bounds2.NOTHING;      // for this node and its children, in "parent" coordinates
    this._selfBounds = Bounds2.NOTHING;  // just for this node, in "local" coordinates
    this._childBounds = Bounds2.NOTHING; // just for children, in "local" coordinates
    this._boundsDirty = true;
    this._selfBoundsDirty = this.isPainted();
    this._childBoundsDirty = true;
    
    // Similar to bounds, but includes any mouse/touch areas respectively. They are validated separately (immediately after normal bounds validation),
    // and are only non-null if there are mouseAreas/touchAreas in this node or any descendants. null indicates that the normal bounds can be treated
    // as the mouse/touch bounds, and hit pruning can use those instead. These are needed because mouse/touch areas (and thus pruning bounds) can be
    // larger than the actual bounds (display bounds, _bounds above)
    this._mouseBounds = null;
    this._mouseBoundsDirty = true;
    this._touchBounds = null;
    this._touchBoundsDirty = true;
    
    // dirty region handling
    this._paintDirty = false;        // whether the self paint is dirty (just this node, none of its children)
    this._subtreePaintDirty = false; // whether the subtree paint is dirty (this node and its children, usually after a transform)
    this._childPaintDirty = false;   // whether the child paint is dirty (excluding self paint, just used for finding _paintDirty, _selfPaintDirty)
    
    // what type of renderer should be forced for this node.
    this._renderer = null;
    this._rendererOptions = null; // options that will determine the layer type
    this._rendererLayerType = null; // cached layer type that is used by the LayerStrategy
    
    // whether layers should be split before and/or after this node. setting both will put this node and its children into a separate layer
    this._layerSplitBefore = false;
    this._layerSplitAfter = false;
    
    if ( options ) {
      this.mutate( options );
    }
    
    phetAllocation && phetAllocation( 'Node' );
  };
  var Node = scenery.Node;
  
  Node.prototype = {
    constructor: Node,
    
    insertChild: function( index, node ) {
      sceneryAssert && sceneryAssert( node !== null && node !== undefined, 'insertChild cannot insert a null/undefined child' );
      sceneryAssert && sceneryAssert( !_.contains( this._children, node ), 'Parent already contains child' );
      sceneryAssert && sceneryAssert( node !== this, 'Cannot add self as a child' );
      
      node._parents.push( this );
      this._children.splice( index, 0, node );
      
      node.invalidateBounds();
      this._boundsDirty = true; // like calling this.invalidateBounds(), but we already marked all ancestors with dirty child bounds
      
      this.markForInsertion( node, index );
      this.notifyStitch( false );
      
      node.invalidateSubtreePaint();
    },
    
    addChild: function( node ) {
      this.insertChild( this._children.length, node );
    },
    
    removeChild: function( node ) {
      sceneryAssert && sceneryAssert( node );
      sceneryAssert && sceneryAssert( this.isChild( node ) );
      
      var indexOfChild = _.indexOf( this._children, node );
      
      this.removeChildWithIndex( node, indexOfChild );
    },
    
    removeChildAt: function( index ) {
      sceneryAssert && sceneryAssert( index >= 0 );
      sceneryAssert && sceneryAssert( index < this._children.length );
      
      var node = this._children[index];
      
      this.removeChildWithIndex( node, index );
    },
    
    // meant for internal use
    removeChildWithIndex: function( node, indexOfChild ) {
      sceneryAssert && sceneryAssert( node );
      sceneryAssert && sceneryAssert( this.isChild( node ) );
      sceneryAssert && sceneryAssert( this._children[indexOfChild] === node );
      
      node.markOldPaint( false );
      
      var indexOfParent = _.indexOf( node._parents, this );
      
      this.markForRemoval( node, indexOfChild );
      
      node._parents.splice( indexOfParent, 1 );
      this._children.splice( indexOfChild, 1 );
      
      this.invalidateBounds();
      this._childBoundsDirty = true; // force recomputation of child bounds after removing a child
      
      this.notifyStitch( false );
    },
    
    removeAllChildren: function() {
      this.setChildren( [] );
    },
    
    // TODO: efficiency by batching calls?
    setChildren: function( children ) {
      if ( this._children !== children ) {
        // remove all children in a way where we don't have to copy the child array for safety
        while ( this._children.length ) {
          this.removeChild( this._children[this._children.length-1] );
        }
        
        var len = children.length;
        for ( var i = 0; i < len; i++ ) {
          this.addChild( children[i] );
        }
      }
    },
    
    getChildren: function() {
      return this._children.slice( 0 ); // create a defensive copy
    },
    
    getChildrenCount: function() {
      return this._children.length;
    },
    
    getParents: function() {
      return this._parents.slice( 0 ); // create a defensive copy
    },
    
    // returns a single parent if it exists, otherwise null (no parents), or an assertion failure (multiple parents)
    getParent: function() {
      sceneryAssert && sceneryAssert( this._parents.length <= 1, 'Cannot call getParent on a node with multiple parents' );
      return this._parents.length ? this._parents[0] : null;
    },
    
    getChildAt: function( index ) {
      return this._children[index];
    },
    
    indexOfParent: function( parent ) {
      return _.indexOf( this._parents, parent );
    },
    
    indexOfChild: function( child ) {
      return _.indexOf( this._children, child );
    },
    
    moveToFront: function() {
      var self = this;
      _.each( this._parents.slice( 0 ), function( parent ) {
        parent.moveChildToFront( self );
      } );
    },
    
    moveChildToFront: function( child ) {
      if ( this.indexOfChild( child ) !== this._children.length - 1 ) {
        this.removeChild( child );
        this.addChild( child );
      }
    },
    
    moveToBack: function() {
      var self = this;
      _.each( this._parents.slice( 0 ), function( parent ) {
        parent.moveChildToBack( self );
      } );
    },
    
    moveChildToBack: function( child ) {
      if ( this.indexOfChild( child ) !== 0 ) {
        this.removeChild( child );
        this.insertChild( 0, child );
      }
    },
    
    // remove this node from its parents
    detach: function() {
      var that = this;
      _.each( this._parents.slice( 0 ), function( parent ) {
        parent.removeChild( that );
      } );
    },
    
    // currently, there is no way to remove peers. if a string is passed as the element pattern, it will be turned into an element
    addPeer: function( element, options ) {
      sceneryAssert && sceneryAssert( !this.instances.length, 'Cannot call addPeer after a node has instances (yet)' );
      
      this._peers.push( { element: element, options: options } );
    },

    /**               
     * @param property any object that has es5 getter for 'value' es5 setter for value, and 
     */
    addLiveRegion: function( property, options ) {
      this._liveRegions.push( {property: property, options: options} );
    },
    
    // should be overridden to modify (increase ONLY if Canvas is involved) the node's bounds. Return the expanded bounds.
    overrideBounds: function( computedBounds ) {
      return computedBounds;
    },
    
    // ensure that cached bounds stored on this node (and all children) are accurate
    validateBounds: function() {
      var that = this;
      var i;
      
      if ( this._selfBoundsDirty ) {
        // note: this should only be triggered if the bounds were actually changed, since we have a guard in place at invalidateSelf()
        this._selfBoundsDirty = false;
        
        // TODO: consider changing to parameter object (that may be a problem for the GC overhead)
        this.fireEvent( 'selfBounds', this._selfBounds );
      }
      
      // validate bounds of children if necessary
      if ( this._childBoundsDirty ) {
        
        // have each child validate their own bounds
        i = this._children.length;
        while ( i-- ) {
          this._children[i].validateBounds();
        }
        
        var oldChildBounds = this._childBounds;
        
        // and recompute our _childBounds
        this._childBounds = Bounds2.NOTHING.copy();
        
        i = this._children.length;
        while ( i-- ) {
          this._childBounds.includeBounds( this._children[i]._bounds );
        }
        
        // run this before firing the event
        this._childBoundsDirty = false;
        
        if ( !this._childBounds.equals( oldChildBounds ) ) {
          // TODO: consider changing to parameter object (that may be a problem for the GC overhead)
          this.fireEvent( 'childBounds', this._childBounds );
        }
      }
      
      // TODO: layout here?
      
      if ( this._boundsDirty ) {
        // run this before firing the event
        this._boundsDirty = false;
        
        var oldBounds = this._bounds;
        
        // converts local to parent bounds. mutable methods used to minimize number of created bounds instances (we create one so we don't change references to the old one)
        var newBounds = this.transformBoundsFromLocalToParent( this._selfBounds.copy().includeBounds( this._childBounds ) );
        newBounds = this.overrideBounds( newBounds ); // allow expansion of the bounds area
        var changed = !newBounds.equals( oldBounds );
        
        if ( changed ) {
          this._bounds = newBounds;
          
          i = this._parents.length;
          while ( i-- ) {
            this._parents[i].invalidateBounds();
          }
          
          // TODO: consider changing to parameter object (that may be a problem for the GC overhead)
          this.fireEvent( 'bounds', this._bounds );
        }
      }
      
      // if there were side-effects, run the validation again until we are clean
      if ( this._selfBoundsDirty || this._childBoundsDirty || this._boundsDirty ) {
        // TODO: if there are side-effects in listeners, this could overflow the stack. we should report an error instead of locking up
        this.validateBounds();
      }
      
      // double-check that all of our bounds handling has been accurate
      if ( sceneryAssertExtra ) {
        // new scope for safety
        (function(){
          var epsilon = 0.000001;
          
          var childBounds = Bounds2.NOTHING.copy();
          _.each( that.children, function( child ) { childBounds.includeBounds( child._bounds ); } );
          
          var fullBounds = that.localToParentBounds( that._selfBounds ).union( that.localToParentBounds( childBounds ) );
          
          sceneryAssertExtra && sceneryAssertExtra( that._childBounds.equalsEpsilon( childBounds, epsilon ), 'Child bounds mismatch after validateBounds: ' +
                                                                                                    that._childBounds.toString() + ', expected: ' + childBounds.toString() );
          sceneryAssertExtra && sceneryAssertExtra( that._bounds.equalsEpsilon( fullBounds, epsilon ) ||
                                                    that._bounds.equalsEpsilon( that.overrideBounds( fullBounds ), epsilon ),
                                                    'Bounds mismatch after validateBounds: ' + that._bounds.toString() + ', expected: ' + fullBounds.toString() );
        })();
      }
    },
    
    validateMouseBounds: function() {
      var that = this;
      
      sceneryAssert && sceneryAssert( !this._selfBoundsDirty && !this._childBoundsDirty && !this._boundsDirty, 'Bounds must be validated before calling validateMouseBounds' );
      
      if ( this._mouseBoundsDirty ) {
        var hasMouseAreas = false;
        
        this._mouseBounds = this._selfBounds.copy(); // start with the self bounds, then add from there
        
        // union of all children's mouse bounds (if they exist)
        var i = this._children.length;
        while ( i-- ) {
          var child = this._children[i];
          child.validateMouseBounds();
          if ( child._mouseBounds ) {
            hasMouseAreas = true;
            that._mouseBounds.includeBounds( child._mouseBounds );
          }
        }
        
        // do this before the transformation to the parent coordinate frame
        if ( this._mouseArea ) {
          hasMouseAreas = true;
          this._mouseBounds.includeBounds( this._mouseArea.bounds );
        }
        
        if ( hasMouseAreas ) {
          // transform it to the parent coordinate frame\
          this.transformBoundsFromLocalToParent( this._mouseBounds );
          
          // and include the normal bounds, so that we don't have to 
          this._mouseBounds.includeBounds( this._bounds );
        } else {
          this._mouseBounds = null; // no mouse areas under this node
        }
        
        this._mouseBoundsDirty = false;
      }
    },
    
    validateTouchBounds: function() {
      var that = this;
      
      sceneryAssert && sceneryAssert( !this._selfBoundsDirty && !this._childBoundsDirty && !this._boundsDirty, 'Bounds must be validated before calling validateTouchBounds' );
      
      if ( this._touchBoundsDirty ) {
        var hasTouchAreas = false;
        
        this._touchBounds = this._selfBounds.copy(); // start with the self bounds, then add from there
        
        // union of all children's touch bounds (if they exist)
        var i = this._children.length;
        while ( i-- ) {
          var child = this._children[i];
          child.validateTouchBounds();
          if ( child._touchBounds ) {
            hasTouchAreas = true;
            that._touchBounds.includeBounds( child._touchBounds );
          }
        }
        
        // do this before the transformation to the parent coordinate frame
        if ( this._touchArea ) {
          hasTouchAreas = true;
          this._touchBounds.includeBounds( this._touchArea.bounds );
        }
        
        if ( hasTouchAreas ) {
          // transform it to the parent coordinate frame
          this.transformBoundsFromLocalToParent( this._touchBounds );
          
          // and include the normal bounds, so that we don't have to 
          this._touchBounds.includeBounds( this._bounds );
        } else {
          this._touchBounds = null; // no touch areas under this node
        }
        
        this._touchBoundsDirty = false;
      }
    },
    
    validatePaint: function() {
      if ( this._paintDirty ) {
        sceneryAssert && sceneryAssert( this.isPainted(), 'Only painted nodes can have self dirty paint' );
        if ( !this._subtreePaintDirty ) {
          // if the subtree is clean, just notify the self (only will hit one layer, instead of possibly multiple ones)
          this.notifyDirtySelfPaint();
        }
        this._paintDirty = false;
      }
      
      if ( this._subtreePaintDirty ) {
        this.notifyDirtySubtreePaint();
        this._subtreePaintDirty = false;
      }
      
      // clear flags and recurse
      if ( this._childPaintDirty ) {
        this._childPaintDirty = false;
        
        var children = this._children;
        var length = children.length;
        for ( var i = 0; i < length; i++ ) {
          children[i].validatePaint();
        }
      }
    },
    
    // mark the bounds of this node as invalid, so it is recomputed before it is accessed again
    invalidateBounds: function() {
      this._boundsDirty = true;
      this._mouseBoundsDirty = true;
      this._touchBoundsDirty = true;
      
      // and set flags for all ancestors
      var i = this._parents.length;
      while ( i-- ) {
        this._parents[i].invalidateChildBounds();
      }
    },
    
    // recursively tag all ancestors with _childBoundsDirty
    invalidateChildBounds: function() {
      // don't bother updating if we've already been tagged
      if ( !this._childBoundsDirty ) {
        this._childBoundsDirty = true;
        this._mouseBoundsDirty = true;
        this._touchBoundsDirty = true;
        var i = this._parents.length;
        while ( i-- ) {
          this._parents[i].invalidateChildBounds();
        }
      }
    },
    
    // mark the paint of this node as invalid, so its new region will be painted
    invalidatePaint: function() {
      sceneryAssert && sceneryAssert( this.isPainted(), 'Can only call invalidatePaint on a painted node' );
      this._paintDirty = true;
      
      // and set flags for all ancestors
      var i = this._parents.length;
      while ( i-- ) {
        this._parents[i].invalidateChildPaint();
      }
    },
    
    invalidateSubtreePaint: function() {
      this._subtreePaintDirty = true;
      
      // and set flags for all ancestors
      var i = this._parents.length;
      while ( i-- ) {
        this._parents[i].invalidateChildPaint();
      }
    },
    
    // recursively tag all ancestors with _childPaintDirty
    invalidateChildPaint: function() {
      // don't bother updating if we've already been tagged
      if ( !this._childPaintDirty ) {
        this._childPaintDirty = true;
        var i = this._parents.length;
        while ( i-- ) {
          this._parents[i].invalidateChildPaint();
        }
      }
    },
    
    // called to notify that self rendering will display different paint, with possibly different bounds
    invalidateSelf: function( newBounds ) {
      sceneryAssert && sceneryAssert( newBounds.isEmpty() || newBounds.isFinite() , "Bounds must be empty or finite in invalidateSelf");
      
      // mark the old region to be repainted, regardless of whether the actual bounds change
      this.notifyBeforeSelfChange();
      
      // if these bounds are different than current self bounds
      if ( !this._selfBounds.equals( newBounds ) ) {
        // set repaint flags
        this._selfBoundsDirty = true;
        this.invalidateBounds();
        
        // record the new bounds
        this._selfBounds = newBounds;
      }
      
      this.invalidatePaint();
    },
    
    markOldSelfPaint: function() {
      this.notifyBeforeSelfChange();
    },
    
    // should be called whenever something triggers changes for how this node is layered
    markLayerRefreshNeeded: function() {
      this.markForLayerRefresh();
      this.notifyStitch( true );
    },
    
    // marks the last-rendered bounds of this node and optionally all of its descendants as needing a repaint
    markOldPaint: function( justSelf ) {
      // TODO: rearchitecture
      if ( justSelf ) {
        this.notifyBeforeSelfChange();
      } else {
        this.notifyBeforeSubtreeChange();
      }
    },
    
    isChild: function( potentialChild ) {
      var ourChild = _.contains( this._children, potentialChild );
      var itsParent = _.contains( potentialChild._parents, this );
      sceneryAssert && sceneryAssert( ourChild === itsParent );
      return ourChild;
    },
    
    // the bounds for self content in "local" coordinates.
    getSelfBounds: function() {
      return this._selfBounds;
    },
    
    getChildBounds: function() {
      this.validateBounds();
      return this._childBounds;
    },
    
    // the bounds for content in render(), in "parent" coordinates
    getBounds: function() {
      this.validateBounds();
      return this._bounds;
    },
    
    // like getBounds() in the "parent" coordinate frame, but includes only visible nodes
    getVisibleBounds: function() {
      // defensive copy, since we use mutable modifications below
      var bounds = this._selfBounds.copy();
      
      var i = this._children.length;
      while ( i-- ) {
        var child = this._children[i];
        if ( child.isVisible() ) {
          bounds.includeBounds( child.getVisibleBounds() );
        }
      }
      
      sceneryAssert && sceneryAssert( bounds.isFinite() || bounds.isEmpty(), 'Visible bounds should not be infinite' );
      return this.localToParentBounds( bounds );
    },
    
    trailUnderPointer: function( pointer ) {
      var options = {};
      if ( pointer.isMouse ) { options.isMouse = true; }
      if ( pointer.isTouch ) { options.isTouch = true; }
      if ( pointer.isPen ) { options.isPen = true; }
      
      return this.trailUnderPoint( pointer.point, options );
    },
    
    /*
     * Return a trail to the top node (if any, otherwise null) whose self-rendered area contains the
     * point (in parent coordinates).
     *
     * If options.pruneInvisible is false, invisible nodes will be allowed in the trail.
     * If options.pruneUnpickable is false, unpickable nodes will be allowed in the trail.
     *
     * When calling, don't pass the recursive flag. It signals that the point passed can be mutated
     */
    trailUnderPoint: function( point, options, recursive ) {
      sceneryAssert && sceneryAssert( point, 'trailUnderPointer requires a point' );
      
      if ( options === undefined ) { options = {}; }
      
      var pruneInvisible = ( options.pruneInvisible === undefined ) ? true : options.pruneInvisible;
      var pruneUnpickable = ( options.pruneUnpickable === undefined ) ? true : options.pruneUnpickable;
      
      if ( pruneInvisible && !this.isVisible() ) {
        return null;
      }
      if ( pruneUnpickable && !this.isPickable() ) {
        return null;
      }
      
      // update bounds for pruning
      this.validateBounds();
      if ( options.isMouse ) { this.validateMouseBounds(); }
      if ( options.isTouch ) { this.validateTouchBounds(); }
      
      var hasHitAreas = options && ( ( options.isMouse && this._mouseBounds ) || ( options.isTouch && this._touchBounds ) || options.isPen );
      
      // bail quickly if this doesn't hit our computed bounds
      if ( hasHitAreas ? (
            // if we have hit areas, prune based on the respective hit bounds (mouseBounds/touchBounds)
            ( options.isMouse && !this._mouseBounds.containsPoint( point ) ) ||
            ( options.isTouch && !this._touchBounds.containsPoint( point ) )
            // otherwise, prune based on the normal bounds
          ) : !this._bounds.containsPoint( point ) ) {
        return null; // not in our bounds, so this point can't possibly be contained
      }
      
      // temporary result variable, since it's easier to do this way to free the computed point
      var result = null;
      
      // point in the local coordinate frame. computed after the main bounds check, so we can bail out there efficiently
      var localPoint = this._transform.getInverse().multiplyVector2( Vector2.createFromPool( point.x, point.y ) );
      // var localPoint = this.parentToLocalPoint( point );
      
      // check children first, since they are rendered later
      if ( this._children.length > 0 && ( hasHitAreas || this._childBounds.containsPoint( localPoint ) ) ) {
        
        // manual iteration here so we can return directly, and so we can iterate backwards (last node is in front)
        for ( var i = this._children.length - 1; i >= 0; i-- ) {
          var child = this._children[i];
          
          var childHit = child.trailUnderPoint( localPoint, options );
          
          // the child will have the point in its parent's coordinate frame (i.e. this node's frame)
          if ( childHit ) {
            childHit.addAncestor( this, i );
            localPoint.freeToPool();
            return childHit;
          }
        }
      }

      // tests for mouse and touch hit areas before testing containsPointSelf
      if ( hasHitAreas ) {
        if ( options.isMouse && this._mouseArea ) {
          result = this._mouseArea.containsPoint( localPoint ) ? new scenery.Trail( this ) : null;
          localPoint.freeToPool();
          return result;
        }
        if ( ( options.isTouch || options.isPen ) && this._touchArea ) {
          result = this._touchArea.containsPoint( localPoint ) ? new scenery.Trail( this ) : null;
          localPoint.freeToPool();
          return result;
        }
      }
      
      // didn't hit our children, so check ourself as a last resort
      if ( hasHitAreas || this._selfBounds.containsPoint( localPoint ) ) {
        if ( this.containsPointSelf( localPoint ) ) {
          localPoint.freeToPool();
          return new scenery.Trail( this );
        }
      }
      
      // signal no hit
      localPoint.freeToPool();
      return null;
    },
    
    // checking for whether a point (in parent coordinates) is contained in this sub-tree
    containsPoint: function( point ) {
      return this.trailUnderPoint( point ) !== null;
    },
    
    // override for computation of whether a point is inside the self content
    // point is considered to be in the local coordinate frame
    containsPointSelf: function( point ) {
      // if self bounds are not null default to checking self bounds
      return this._selfBounds.containsPoint( point );
    },
    
    // whether this node's self intersects the specified bounds, in the local coordinate frame
    intersectsBoundsSelf: function( bounds ) {
      // if self bounds are not null, child should override this
      return this._selfBounds.intersectsBounds( bounds );
    },
    
    isPainted: function() {
      return false;
    },
    
    hasParent: function() {
      return this._parents.length !== 0;
    },
    
    hasChildren: function() {
      return this._children.length > 0;
    },
    
    walkDepthFirst: function( callback ) {
      callback( this );
      var length = this._children.length;
      for ( var i = 0; i < length; i++ ) {
        this._children[i].walkDepthFirst( callback );
      }
    },
    
    getChildrenWithinBounds: function( bounds ) {
      var result = [];
      var length = this._children.length;
      for ( var i = 0; i < length; i++ ) {
        var child = this._children[i];
        if ( !child._bounds.intersection( bounds ).isEmpty() ) {
          result.push( child );
        }
      }
      return result;
    },
    
    // TODO: set this up with a mix-in for a generic notifier?
    addInputListener: function( listener ) {
      // don't allow listeners to be added multiple times
      if ( _.indexOf( this._inputListeners, listener ) === -1 ) {
        this._inputListeners.push( listener );
      }
      return this;
    },
    
    removeInputListener: function( listener ) {
      // ensure the listener is in our list
      sceneryAssert && sceneryAssert( _.indexOf( this._inputListeners, listener ) !== -1 );
      
      this._inputListeners.splice( _.indexOf( this._inputListeners, listener ), 1 );
      return this;
    },
    
    getInputListeners: function() {
      return this._inputListeners.slice( 0 ); // defensive copy
    },
    
    /*
     * Dispatches an event across all possible Trails ending in this node.
     *
     * For example, if the scene has two children A and B, and both of those nodes have X as a child,
     * dispatching an event on X will fire the event with the following trails:
     * on X     with trail [ X ]
     * on A     with trail [ A, X ]
     * on scene with trail [ scene, A, X ]
     * on B     with trail [ B, X ]
     * on scene with trail [ scene, B, X ]
     *
     * This allows you to add a listener on any node to get notifications for all of the trails that the
     * event is relevant for (e.g. marks dirty paint region for both places X was on the scene).
     */
    dispatchEvent: function( type, args ) {
      sceneryEventLog && sceneryEventLog( this.constructor.name + '.dispatchEvent ' + type );
      var trail = new scenery.Trail();
      trail.setMutable(); // don't allow this trail to be set as immutable for storage
      args.trail = trail; // this reference shouldn't be changed be listeners (or errors will occur)
      
      // store a branching flag, since if we don't branch at all, we don't have to walk our trail back down.
      var branches = false;
      
      function recursiveEventDispatch( node ) {
        trail.addAncestor( node );
        
        node.fireEvent( type, args );
        
        var parents = node._parents;
        var length = parents.length;
        
        // make sure to set the branch flag here before iterating (don't move it)
        branches = branches || length > 1;
        
        for ( var i = 0; i < length; i++ ) {
          recursiveEventDispatch( parents[i] );
        }
        
        // if there were no branches, we will not fire another listener once we have reached here
        if ( branches ) {
          trail.removeAncestor();
        }
      }
      
      recursiveEventDispatch( this );
    },
    
    // TODO: consider renaming to translateBy to match scaleBy
    translate: function( x, y, prependInstead ) {
      if ( typeof x === 'number' ) {
        // translate( x, y, prependInstead )
        if ( !x && !y ) { return; } // bail out if both are zero
        if ( prependInstead ) {
          this.prependTranslation( x, y );
        } else {
          this.appendMatrix( Matrix3.translation( x, y ) );
        }
      } else {
        // translate( vector, prependInstead )
        var vector = x;
        if ( !vector.x && !vector.y ) { return; } // bail out if both are zero
        this.translate( vector.x, vector.y, y ); // forward to full version
      }
    },
    
    // scale( s ) is also supported, which will scale both dimensions by the same amount. renamed from 'scale' to satisfy the setter/getter
    scale: function( x, y, prependInstead ) {
      if ( typeof x === 'number' ) {
        if ( y === undefined ) {
          // scale( scale )
          if ( x === 1 ) { return; } // bail out if we are scaling by 1 (identity)
          this.appendMatrix( Matrix3.scaling( x, x ) );
        } else {
          // scale( x, y, prependInstead )
          if ( x === 1 && y === 1 ) { return; } // bail out if we are scaling by 1 (identity)
          if ( prependInstead ) {
            this.prependMatrix( Matrix3.scaling( x, y ) );
          } else {
            this.appendMatrix( Matrix3.scaling( x, y ) );
          }
        }
      } else {
        // scale( vector, prependInstead ) or scale( { x: x, y: y }, prependInstead )
        var vector = x;
        this.scale( vector.x, vector.y, y ); // forward to full version
      }
    },
    
    // TODO: consider naming to rotateBy to match scaleBy (due to scale property / method name conflict)
    rotate: function( angle, prependInstead ) {
      if ( angle % ( 2 * Math.PI ) === 0 ) { return; } // bail out if our angle is effectively 0
      if ( prependInstead ) {
        this.prependMatrix( Matrix3.rotation2( angle ) );
      } else {
        this.appendMatrix( Matrix3.rotation2( angle ) );
      }
    },
    
    // point should be in the parent coordinate frame
    // TODO: determine whether this should use the appendMatrix method
    rotateAround: function( point, angle ) {
      var matrix = Matrix3.translation( -point.x, -point.y );
      matrix = Matrix3.rotation2( angle ).timesMatrix( matrix );
      matrix = Matrix3.translation( point.x, point.y ).timesMatrix( matrix );
      this.prependMatrix( matrix );
    },
    
    getX: function() {
      return this._transform.getMatrix().m02();
    },
    
    setX: function( x ) {
      sceneryAssert && sceneryAssert( typeof x === 'number' );
      
      this.translate( x - this.getX(), 0, true );
      return this;
    },
    
    getY: function() {
      return this._transform.getMatrix().m12();
    },
    
    setY: function( y ) {
      sceneryAssert && sceneryAssert( typeof y === 'number' );
      
      this.translate( 0, y - this.getY(), true );
      return this;
    },
    
    // returns a vector with an entry for each axis, e.g. (5,2) for an Affine-style matrix with rows ((5,0,0),(0,2,0),(0,0,1))
    getScaleVector: function() {
      return this._transform.getMatrix().getScaleVector();
    },
    
    // supports setScaleMagnitude( 5 ) for both dimensions, setScaleMagnitude( 5, 3 ) for each dimension separately, or setScaleMagnitude( new Vector2( x, y ) )
    setScaleMagnitude: function( a, b ) {
      var currentScale = this.getScaleVector();
      
      if ( typeof a === 'number' ) {
        if ( b === undefined ) {
          // to map setScaleMagnitude( scale ) => setScaleMagnitude( scale, scale )
          b = a;
        }
        // setScaleMagnitude( x, y )
        this.appendMatrix( Matrix3.scaling( a / currentScale.x, b / currentScale.y ) );
      } else {
        // setScaleMagnitude( vector ), where we set the x-scale to vector.x and y-scale to vector.y
        this.appendMatrix( Matrix3.scaling( a.x / currentScale.x, a.y / currentScale.y ) );
      }
      return this;
    },
    
    getRotation: function() {
      return this._transform.getMatrix().getRotation();
    },
    
    setRotation: function( rotation ) {
      sceneryAssert && sceneryAssert( typeof rotation === 'number' );
      
      this.appendMatrix( Matrix3.rotation2( rotation - this.getRotation() ) );
      return this;
    },
    
    // supports setTranslation( x, y ) or setTranslation( new Vector2( x, y ) ) .. or technically setTranslation( { x: x, y: y } )
    setTranslation: function( a, b ) {
      var m = this._transform.getMatrix();
      var tx = m.m02();
      var ty = m.m12();

      var dx, dy;
      
      if ( typeof a === 'number' ) {
        dx = a - tx;
        dy = b - ty;
      } else {
        dx = a.x - tx;
        dy = a.y - ty;
      }
      
      this.translate( dx, dy, true );
      
      return this;
    },
    
    getTranslation: function() {
      var matrix = this._transform.getMatrix();
      return new Vector2( matrix.m02(), matrix.m12() );
    },
    
    // append a transformation matrix to our local transform
    appendMatrix: function( matrix ) {
      this._transform.append( matrix );
    },
    
    // prepend a transformation matrix to our local transform
    prependMatrix: function( matrix ) {
      this._transform.prepend( matrix );
    },

    // prepend an x,y translation to our local transform without allocating a matrix for it, see #119
    prependTranslation: function( x,y ) {
      this._transform.prependTranslation( x, y );
    },
    
    setMatrix: function( matrix ) {
      this._transform.set( matrix );
    },
    
    getMatrix: function() {
      return this._transform.getMatrix();
    },
    
    // change the actual transform reference (not just the actual transform)
    setTransform: function( transform ) {
      sceneryAssert && sceneryAssert( transform.isFinite(), 'Transform should not have infinite/NaN values' );
      
      if ( this._transform !== transform ) {
        // since our referenced transform doesn't change, we need to trigger the before/after ourselves
        this.beforeTransformChange();
        
        // swap the transform and move the listener to the new one
        this._transform.removeTransformListener( this._transformListener ); // don't leak memory!
        this._transform = transform;
        this._transform.prependTransformListener( this._transformListener );
        
        this.afterTransformChange();
      }
    },
    
    getTransform: function() {
      // for now, return an actual copy. we can consider listening to changes in the future
      return this._transform;
    },
    
    resetTransform: function() {
      this.setMatrix( Matrix3.IDENTITY );
    },
    
    // called before our transform is changed
    beforeTransformChange: function() {
      // mark our old bounds as dirty, so that any dirty region repainting will include not just our new position, but also our old position
      this.notifyBeforeSubtreeChange();
    },
    
    // called after our transform is changed
    afterTransformChange: function() {
      this.notifyTransformChange();
      
      this.invalidateBounds();
      this.invalidateSubtreePaint();
    },
    
    // the left bound of this node, in the parent coordinate frame
    getLeft: function() {
      return this.getBounds().minX;
    },
    
    // shifts this node horizontally so that its left bound (in the parent coordinate frame) is 'left'
    setLeft: function( left ) {
      sceneryAssert && sceneryAssert( typeof left === 'number' );
      
      this.translate( left - this.getLeft(), 0, true );
      return this; // allow chaining
    },
    
    // the right bound of this node, in the parent coordinate frame
    getRight: function() {
      return this.getBounds().maxX;
    },
    
    // shifts this node horizontally so that its right bound (in the parent coordinate frame) is 'right'
    setRight: function( right ) {
      sceneryAssert && sceneryAssert( typeof right === 'number' );
      
      this.translate( right - this.getRight(), 0, true );
      return this; // allow chaining
    },
    
    getCenter: function() {
      return this.getBounds().getCenter();
    },
    
    setCenter: function( center ) {
      sceneryAssert && sceneryAssert( center instanceof Vector2 );
      
      this.translate( center.minus( this.getCenter() ), true );
      return this;
    },
    
    getCenterX: function() {
      return this.getBounds().getCenterX();
    },
    
    setCenterX: function( x ) {
      sceneryAssert && sceneryAssert( typeof x === 'number' );
      
      this.translate( x - this.getCenterX(), 0, true );
      return this; // allow chaining
    },
    
    getCenterY: function() {
      return this.getBounds().getCenterY();
    },
    
    setCenterY: function( y ) {
      sceneryAssert && sceneryAssert( typeof y === 'number' );
      
      this.translate( 0, y - this.getCenterY(), true );
      return this; // allow chaining
    },
    
    // the top bound of this node, in the parent coordinate frame
    getTop: function() {
      return this.getBounds().minY;
    },
    
    // shifts this node vertically so that its top bound (in the parent coordinate frame) is 'top'
    setTop: function( top ) {
      sceneryAssert && sceneryAssert( typeof top === 'number' );
      
      this.translate( 0, top - this.getTop(), true );
      return this; // allow chaining
    },
    
    // the bottom bound of this node, in the parent coordinate frame
    getBottom: function() {
      return this.getBounds().maxY;
    },
    
    // shifts this node vertically so that its bottom bound (in the parent coordinate frame) is 'bottom'
    setBottom: function( bottom ) {
      sceneryAssert && sceneryAssert( typeof bottom === 'number' );
      
      this.translate( 0, bottom - this.getBottom(), true );
      return this; // allow chaining
    },
    
    getWidth: function() {
      return this.getBounds().getWidth();
    },
    
    getHeight: function() {
      return this.getBounds().getHeight();
    },
    
    getId: function() {
      return this._id;
    },
    
    isVisible: function() {
      return this._visible;
    },
    
    setVisible: function( visible ) {
      sceneryAssert && sceneryAssert( typeof visible === 'boolean' );
      
      if ( visible !== this._visible ) {
        if ( this._visible ) {
          this.notifyBeforeSubtreeChange();
        }
        
        this._visible = visible;
        
        this.notifyVisibilityChange();
      }
      return this;
    },
    
    getOpacity: function() {
      return this._opacity;
    },
    
    setOpacity: function( opacity ) {
      sceneryAssert && sceneryAssert( typeof opacity === 'number' );
      
      var clampedOpacity = clamp( opacity, 0, 1 );
      if ( clampedOpacity !== this._opacity ) {
        this.notifyBeforeSubtreeChange();
        
        this._opacity = clampedOpacity;
        
        this.notifyOpacityChange();
      }
    },
    
    isPickable: function() {
      return this._pickable;
    },
    
    setPickable: function( pickable ) {
      sceneryAssert && sceneryAssert( typeof pickable === 'boolean' );
      
      if ( this._pickable !== pickable ) {
        // no paint or invalidation changes for now, since this is only handled for the mouse
        this._pickable = pickable;
        
        // TODO: invalidate the cursor somehow?
      }
    },
    
    setCursor: function( cursor ) {
      sceneryAssert && sceneryAssert( typeof cursor === 'string' || cursor === null );
      
      // TODO: consider a mapping of types to set reasonable defaults
      /*
      auto default none inherit help pointer progress wait crosshair text vertical-text alias copy move no-drop not-allowed
      e-resize n-resize w-resize s-resize nw-resize ne-resize se-resize sw-resize ew-resize ns-resize nesw-resize nwse-resize
      context-menu cell col-resize row-resize all-scroll url( ... ) --> does it support data URLs?
       */
      
      // allow the 'auto' cursor type to let the ancestors or scene pick the cursor type
      this._cursor = cursor === "auto" ? null : cursor;
    },
    
    getCursor: function() {
      return this._cursor;
    },
    
    setMouseArea: function( shape ) {
      sceneryAssert && sceneryAssert( shape === null || shape instanceof Shape, 'mouseArea needs to be a kite.Shape, or null' );
      
      if ( this._mouseArea !== shape ) {
        this._mouseArea = shape; // TODO: could change what is under the mouse, invalidate!
        
        this.invalidateBounds();
      }
    },
    
    getMouseArea: function() {
      return this._mouseArea;
    },
    
    setTouchArea: function( shape ) {
      sceneryAssert && sceneryAssert( shape === null || shape instanceof Shape, 'touchArea needs to be a kite.Shape, or null' );
      
      if ( this._touchArea !== shape ) {
        this._touchArea = shape; // TODO: could change what is under the touch, invalidate!
        
        this.invalidateBounds();
      }
    },
    
    getTouchArea: function() {
      return this._touchArea;
    },
    
    updateLayerType: function() {
      if ( this._renderer && this._rendererOptions ) {
        // TODO: factor this check out! Make RendererOptions its own class?
        // TODO: FIXME: support undoing this!
        // ensure that if we are passing a CSS transform, we pass this node as the baseNode
        if ( this._rendererOptions.cssTransform || this._rendererOptions.cssTranslation || this._rendererOptions.cssRotation || this._rendererOptions.cssScale ) {
          this._rendererOptions.baseNode = this;
        } else if ( this._rendererOptions.hasOwnProperty( 'baseNode' ) ) {
          delete this._rendererOptions.baseNode; // don't override, let the scene pass in the scene
        }
        // if we set renderer and rendererOptions, only then do we want to trigger a specific layer type
        this._rendererLayerType = this._renderer.createLayerType( this._rendererOptions );
      } else {
        this._rendererLayerType = null; // nothing signaled, since we want to support multiple layer types (including if we specify a renderer)
      }
    },
    
    getRendererLayerType: function() {
      return this._rendererLayerType;
    },
    
    hasRendererLayerType: function() {
      return !!this._rendererLayerType;
    },
    
    setRenderer: function( renderer ) {
      var newRenderer;
      if ( typeof renderer === 'string' ) {
        sceneryAssert && sceneryAssert( scenery.Renderer[renderer], 'unknown renderer in setRenderer: ' + renderer );
        newRenderer = scenery.Renderer[renderer];
      } else if ( renderer instanceof scenery.Renderer ) {
        newRenderer = renderer;
      } else if ( !renderer ) {
        newRenderer = null;
      } else {
        throw new Error( 'unrecognized type of renderer: ' + renderer );
      }
      if ( newRenderer !== this._renderer ) {
        sceneryAssert && sceneryAssert( !this.isPainted() || !newRenderer || _.contains( this._supportedRenderers, newRenderer ), 'renderer ' + newRenderer + ' not supported by ' + this.constructor.name );
        this._renderer = newRenderer;
        
        this.updateLayerType();
        this.markLayerRefreshNeeded();
      }
    },
    
    getRenderer: function() {
      return this._renderer;
    },
    
    hasRenderer: function() {
      return !!this._renderer;
    },
    
    setRendererOptions: function( options ) {
      // TODO: consider checking options based on the specified 'renderer'?
      this._rendererOptions = options;
      
      this.updateLayerType();
      this.markLayerRefreshNeeded();
    },
    
    getRendererOptions: function() {
      return this._rendererOptions;
    },
    
    hasRendererOptions: function() {
      return !!this._rendererOptions;
    },
    
    setLayerSplitBefore: function( split ) {
      sceneryAssert && sceneryAssert( typeof split === 'boolean' );
      
      if ( this._layerSplitBefore !== split ) {
        this._layerSplitBefore = split;
        this.markLayerRefreshNeeded();
      }
    },
    
    isLayerSplitBefore: function() {
      return this._layerSplitBefore;
    },
    
    setLayerSplitAfter: function( split ) {
      sceneryAssert && sceneryAssert( typeof split === 'boolean' );
      
      if ( this._layerSplitAfter !== split ) {
        this._layerSplitAfter = split;
        this.markLayerRefreshNeeded();
      }
    },
    
    isLayerSplitAfter: function() {
      return this._layerSplitAfter;
    },
    
    setLayerSplit: function( split ) {
      sceneryAssert && sceneryAssert( typeof split === 'boolean' );
      
      if ( split !== this._layerSplitBefore || split !== this._layerSplitAfter ) {
        this._layerSplitBefore = split;
        this._layerSplitAfter = split;
        this.markLayerRefreshNeeded();
      }
    },
    
    // returns a unique trail (if it exists) where each node in the ancestor chain has 0 or 1 parents
    getUniqueTrail: function() {
      var trail = new scenery.Trail();
      var node = this;
      
      while ( node ) {
        trail.addAncestor( node );
        sceneryAssert && sceneryAssert( node._parents.length <= 1 );
        node = node._parents[0]; // should be undefined if there aren't any parents
      }
      
      return trail;
    },
    
    // all nodes in the connected component, returned in an arbitrary order
    getConnectedNodes: function() {
      var result = [];
      var fresh = this._children.concat( this._parents ).concat( this );
      while ( fresh.length ) {
        var node = fresh.pop();
        if ( !_.contains( result, node ) ) {
          result.push( node );
          fresh = fresh.concat( node._children, node._parents );
        }
      }
      return result;
    },
    
    getTopologicallySortedNodes: function() {
      // see http://en.wikipedia.org/wiki/Topological_sorting
      var edges = {};
      var s = [];
      var l = [];
      var n;
      _.each( this.getConnectedNodes(), function( node ) {
        edges[node.id] = {};
        _.each( node.children, function( m ) {
          edges[node.id][m.id] = true;
        } );
        if ( !node.parents.length ) {
          s.push( node );
        }
      } );
      function handleChild( m ) {
        delete edges[n.id][m.id];
        if ( _.every( edges, function( children ) { return !children[m.id]; } ) ) {
          // there are no more edges to m
          s.push( m );
        }
      }
      
      while ( s.length ) {
        n = s.pop();
        l.push( n );
        
        _.each( n.children, handleChild );
      }
      
      // ensure that there are no edges left, since then it would contain a circular reference
      sceneryAssert && sceneryAssert( _.every( edges, function( children ) {
        return _.every( children, function( final ) { return false; } );
      } ), 'circular reference check' );
      
      return l;
    },
    
    // verify that this.addChild( child ) it wouldn't cause circular references
    canAddChild: function( child ) {
      if ( this === child || _.contains( this.children, child ) ) {
        return false;
      }
      
      // see http://en.wikipedia.org/wiki/Topological_sorting
      // TODO: remove duplication with above handling?
      var edges = {};
      var s = [];
      var l = [];
      var n;
      _.each( this.getConnectedNodes().concat( child.getConnectedNodes() ), function( node ) {
        edges[node.id] = {};
        _.each( node.children, function( m ) {
          edges[node.id][m.id] = true;
        } );
        if ( !node.parents.length && node !== child ) {
          s.push( node );
        }
      } );
      edges[this.id][child.id] = true; // add in our 'new' edge
      function handleChild( m ) {
        delete edges[n.id][m.id];
        if ( _.every( edges, function( children ) { return !children[m.id]; } ) ) {
          // there are no more edges to m
          s.push( m );
        }
      }
      
      while ( s.length ) {
        n = s.pop();
        l.push( n );
        
        _.each( n.children, handleChild );
        
        // handle our new edge
        if ( n === this ) {
          handleChild( child );
        }
      }
      
      // ensure that there are no edges left, since then it would contain a circular reference
      return _.every( edges, function( children ) {
        return _.every( children, function( final ) { return false; } );
      } );
    },
    
    debugText: function() {
      var startPointer = new scenery.TrailPointer( new scenery.Trail( this ), true );
      var endPointer = new scenery.TrailPointer( new scenery.Trail( this ), false );
      
      var depth = 0;
      
      startPointer.depthFirstUntil( endPointer, function( pointer ) {
        if ( pointer.isBefore ) {
          // hackish way of multiplying a string
          var padding = new Array( depth * 2 ).join( ' ' );
          console.log( padding + pointer.trail.lastNode().getId() + ' ' + pointer.trail.toString() );
        }
        depth += pointer.isBefore ? 1 : -1;
      }, false );
    },
    
    /*
     * Renders this node to a canvas. If toCanvas( callback ) is used, the canvas will contain the node's
     * entire bounds.
     *
     * callback( canvas, x, y ) is called, where x and y offsets are computed if not specified.
     */
    toCanvas: function( callback, x, y, width, height ) {
      var self = this;
      
      var padding = 2; // padding used if x and y are not set
      
      var bounds = this.getBounds();
      x = x !== undefined ? x : Math.ceil( padding - bounds.minX );
      y = y !== undefined ? y : Math.ceil( padding - bounds.minY );
      width = width !== undefined ? width : Math.ceil( x + bounds.getWidth() + padding );
      height = height !== undefined ? height : Math.ceil( y + bounds.getHeight() + padding );
      
      var canvas = document.createElement( 'canvas' );
      canvas.width = width;
      canvas.height = height;
      var context = canvas.getContext( '2d' );
      
      var $div = $( document.createElement( 'div' ) );
      $div.width( width ).height( height );
      var scene = new scenery.Scene( $div );
      
      scene.addChild( self );
      scene.x = x;
      scene.y = y;
      scene.updateScene();
      
      scene.renderToCanvas( canvas, context, function() {
        callback( canvas, x, y );
        
        // let us be garbage collected
        scene.removeChild( self );
      } );
    },
    
    // gives a data URI, with the same parameter handling as Node.toCanvas()
    toDataURL: function( callback, x, y, width, height ) {
      this.toCanvas( function( canvas, x, y ) {
        // this x and y shadow the outside parameters, and will be different if the outside parameters are undefined
        callback( canvas.toDataURL(), x, y );
      }, x, y, width, height );
    },
    
    // gives an HTMLImageElement with the same parameter handling as Node.toCanvas(). guaranteed to be asynchronous
    toImage: function( callback, x, y, width, height ) {
      this.toDataURL( function( url, x, y ) {
        // this x and y shadow the outside parameters, and will be different if the outside parameters are undefined
        var img = document.createElement( 'img' );
        img.onload = function() {
          callback( img, x, y );
          try {
            delete img.onload;
          } catch ( e ) {} // fails on Safari 5.1
        };
        img.src = url;
      }, x, y, width, height );
    },
    
    // will call callback( node )
    toImageNodeAsynchronous: function( callback, x, y, width, height ) {
      this.toImage( function( image, x, y ) {
        callback( new scenery.Node( { children: [
          new scenery.Image( image, { x: -x, y: -y } )
        ] } ) );
      }, x, y, width, height );
    },
    
    // fully synchronous, but returns a node that can only be rendered in Canvas
    toCanvasNodeSynchronous: function( x, y, width, height ) {
      var result;
      this.toCanvas( function( canvas, x, y ) {
        result = new scenery.Node( { children: [
          new scenery.Image( canvas, { x: -x, y: -y } )
        ] } );
      }, x, y, width, height );
      sceneryAssert && sceneryAssert( result, 'toCanvasNodeSynchronous requires that the node can be rendered only using Canvas' );
      return result;
    },
    
    // synchronous, but Image will not have the correct bounds immediately (that will be asynchronous)
    toDataURLNodeSynchronous: function( x, y, width, height ) {
      var result;
      this.toDataURL( function( dataURL, x, y ) {
        result = new scenery.Node( { children: [
          new scenery.Image( dataURL, { x: -x, y: -y } )
        ] } );
      }, x, y, width, height );
      sceneryAssert && sceneryAssert( result, 'toDataURLNodeSynchronous requires that the node can be rendered only using Canvas' );
      return result;
    },
    
    /*---------------------------------------------------------------------------*
    * Instance handling
    *----------------------------------------------------------------------------*/
    
    getInstances: function() {
      return this._instances;
    },
    
    addInstance: function( instance ) {
      sceneryAssert && sceneryAssert( instance.getNode() === this, 'Must be an instance of this Node' );
      sceneryAssert && sceneryAssert( !_.find( this._instances, function( other ) { return instance.equals( other ); } ), 'Cannot add duplicates of an instance to a Node' );
      this._instances.push( instance );
      if ( this._instances.length === 1 ) {
        this.firstInstanceAdded();
      }
    },
    
    firstInstanceAdded: function() {
      // no-op, meant to be overridden in the prototype chain
    },
    
    // returns undefined if there is no instance.
    getInstanceFromTrail: function( trail ) {
      var result;
      var len = this._instances.length;
      if ( len === 1 ) {
        // don't bother with checking the trail, but assertion should assure that it's what we're looking for
        result = this._instances[0];
      } else {
        var i = len;
        while ( i-- ) {
          if ( this._instances[i].trail.equals( trail ) ) {
            result = this._instances[i];
            break;
          }
        }
        // leave it as undefined if we don't find one
      }
      sceneryAssert && sceneryAssert( result, 'Could not find an instance for the trail ' + trail.toString() );
      sceneryAssert && sceneryAssert( result.trail.equals( trail ), 'Instance has an incorrect Trail' );
      return result;
    },
    
    removeInstance: function( instance ) {
      var index = _.indexOf( this._instances, instance ); // actual instance equality (NOT capitalized, normal meaning)
      sceneryAssert && sceneryAssert( index !== -1, 'Cannot remove an Instance from a Node if it was not there' );
      this._instances.splice( index, 1 );
      if ( this._instances.length === 0 ) {
        this.lastInstanceRemoved();
      }
    },
    
    lastInstanceRemoved: function() {
      // no-op, meant to be overridden in the prototype chain
    },
    
    notifyVisibilityChange: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyVisibilityChange();
      }
    },
    
    notifyOpacityChange: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyOpacityChange();
      }
    },
    
    notifyBeforeSelfChange: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyBeforeSelfChange();
      }
    },
    
    notifyBeforeSubtreeChange: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyBeforeSubtreeChange();
      }
    },
    
    notifyDirtySelfPaint: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyDirtySelfPaint();
      }
    },
    
    notifyDirtySubtreePaint: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyDirtySubtreePaint();
      }
    },
    
    notifyTransformChange: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyTransformChange();
      }
    },
    
    notifyBoundsAccuracyChange: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyBoundsAccuracyChange();
      }
    },
    
    notifyStitch: function( match ) {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].notifyStitch( match );
      }
    },
    
    markForLayerRefresh: function() {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].markForLayerRefresh();
      }
    },
    
    markForInsertion: function( child, index ) {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].markForInsertion( child, index );
      }
    },
    
    markForRemoval: function( child, index ) {
      var i = this._instances.length;
      while ( i-- ) {
        this._instances[i].markForRemoval( child, index );
      }
    },
    
    /*---------------------------------------------------------------------------*
    * Coordinate transform methods
    *----------------------------------------------------------------------------*/
    
    // apply this node's transform to the point
    localToParentPoint: function( point ) {
      return this._transform.transformPosition2( point );
    },
    
    // apply this node's transform to the bounds
    localToParentBounds: function( bounds ) {
      return this._transform.transformBounds2( bounds );
    },
    
    // apply the inverse of this node's transform to the point
    parentToLocalPoint: function( point ) {
      return this._transform.inversePosition2( point );
    },
    
    // apply the inverse of this node's transform to the bounds
    parentToLocalBounds: function( bounds ) {
      return this._transform.inverseBounds2( bounds );
    },
    
    // mutable optimized form of localToParentBounds
    transformBoundsFromLocalToParent: function( bounds ) {
      return bounds.transform( this._transform.getMatrix() );
    },
    
    // mutable optimized form of parentToLocalBounds
    transformBoundsFromParentToLocal: function( bounds ) {
      return bounds.transform( this._transform.getInverse() );
    },
    
    // returns the matrix (fresh copy) that transforms points from the local coordinate frame into the global coordinate frame
    getLocalToGlobalMatrix: function() {
      var node = this;
      
      // we need to apply the transformations in the reverse order, so we temporarily store them
      var matrices = [];
      
      // concatenation like this has been faster than getting a unique trail, getting its transform, and applying it
      while ( node ) {
        matrices.push( node._transform.getMatrix() );
        sceneryAssert && sceneryAssert( node._parents[1] === undefined, 'getLocalToGlobalMatrix unable to work for DAG' );
        node = node._parents[0];
      }
      
      var matrix = new Matrix3(); // will be modified in place
      
      // iterate from the back forwards (from the root node to here)
      for ( var i = matrices.length - 1; i >=0; i-- ) {
        matrix.multiplyMatrix( matrices[i] );
      }
      
      // NOTE: always return a fresh copy, getGlobalToLocalMatrix depends on it to minimize instance usage!
      return matrix;
    },
    
    // equivalent to getUniqueTrail().getTransform(), but faster.
    getUniqueTransform: function() {
      return new Transform3( this.getLocalToGlobalMatrix() );
    },
    
    // returns the matrix (fresh copy) that transforms points in the global coordinate frame into the local coordinate frame
    getGlobalToLocalMatrix: function() {
      return this.getLocalToGlobalMatrix().invert();
    },
    
    // apply this node's transform (and then all of its parents' transforms) to the point
    localToGlobalPoint: function( point ) {
      var node = this;
      var resultPoint = point.copy();
      while ( node ) {
        // in-place multiplication
        node._transform.getMatrix().multiplyVector2( resultPoint );
        sceneryAssert && sceneryAssert( node._parents[1] === undefined, 'localToGlobalPoint unable to work for DAG' );
        node = node._parents[0];
      }
      return resultPoint;
    },
    
    globalToLocalPoint: function( point ) {
      var node = this;
      // TODO: performance: test whether it is faster to get a total transform and then invert (won't compute individual inverses)
      
      // we need to apply the transformations in the reverse order, so we temporarily store them
      var transforms = [];
      while ( node ) {
        transforms.push( node._transform );
        sceneryAssert && sceneryAssert( node._parents[1] === undefined, 'globalToLocalPoint unable to work for DAG' );
        node = node._parents[0];
      }
      
      // iterate from the back forwards (from the root node to here)
      var resultPoint = point.copy();
      for ( var i = transforms.length - 1; i >=0; i-- ) {
        // in-place multiplication
        transforms[i].getInverse().multiplyVector2( resultPoint );
      }
      return resultPoint;
    },
    
    // apply this node's transform (and then all of its parents' transforms) to the bounds
    localToGlobalBounds: function( bounds ) {
      // apply the bounds transform only once, so we can minimize the expansion encountered from multiple rotations
      // it also seems to be a bit faster this way
      return bounds.transformed( this.getLocalToGlobalMatrix() );
    },
    
    globalToLocalBounds: function( bounds ) {
      // apply the bounds transform only once, so we can minimize the expansion encountered from multiple rotations
      return bounds.transformed( this.getGlobalToLocalMatrix() );
    },
    
    // like localToGlobalPoint, but without applying this node's transform
    parentToGlobalPoint: function( point ) {
      sceneryAssert && sceneryAssert( this.parents.length <= 1, 'parentToGlobalPoint unable to work for DAG' );
      return this.parents.length ? this.parents[0].localToGlobalPoint( point ) : point;
    },
    
    // like localToGlobalBounds, but without applying this node's transform
    parentToGlobalBounds: function( bounds ) {
      sceneryAssert && sceneryAssert( this.parents.length <= 1, 'parentToGlobalBounds unable to work for DAG' );
      return this.parents.length ? this.parents[0].localToGlobalBounds( bounds ) : bounds;
    },
    
    globalToParentPoint: function( point ) {
      sceneryAssert && sceneryAssert( this.parents.length <= 1, 'globalToParentPoint unable to work for DAG' );
      return this.parents.length ? this.parents[0].globalToLocalPoint( point ) : point;
    },
    
    globalToParentBounds: function( bounds ) {
      sceneryAssert && sceneryAssert( this.parents.length <= 1, 'globalToParentBounds unable to work for DAG' );
      return this.parents.length ? this.parents[0].globalToLocalBounds( bounds ) : bounds;
    },
    
    // get the Bounds2 of this node in the global coordinate frame.  Does not work for DAG.
    getGlobalBounds: function() {
      sceneryAssert && sceneryAssert( this.parents.length <= 1, 'globalBounds unable to work for DAG' );
      return this.parentToGlobalBounds( this.getBounds() );
    },
    
    // get the Bounds2 of any other node by converting to the global coordinate frame.  Does not work for DAG.
    boundsOf: function( node ) {
      return this.globalToLocalBounds( node.getGlobalBounds() );
    },
    
    // get the Bounds2 of this node in the coordinate frame of the parameter node. Does not work for DAG cases.
    boundsTo: function( node ) {
      return node.globalToLocalBounds( this.getGlobalBounds() );
    },
    
    /*---------------------------------------------------------------------------*
    * ES5 get/set
    *----------------------------------------------------------------------------*/
    
    set layerSplit( value ) { this.setLayerSplit( value ); },
    get layerSplit() { throw new Error( 'You can\'t get a layerSplit property, since it modifies two separate properties' ); },
    
    set layerSplitBefore( value ) { this.setLayerSplitBefore( value ); },
    get layerSplitBefore() { return this.isLayerSplitBefore(); },
    
    set layerSplitAfter( value ) { this.setLayerSplitAfter( value ); },
    get layerSplitAfter() { return this.isLayerSplitAfter(); },
    
    set renderer( value ) { this.setRenderer( value ); },
    get renderer() { return this.getRenderer(); },
    
    set rendererOptions( value ) { this.setRendererOptions( value ); },
    get rendererOptions() { return this.getRendererOptions(); },
    
    set cursor( value ) { this.setCursor( value ); },
    get cursor() { return this.getCursor(); },
    
    set mouseArea( value ) { this.setMouseArea( value ); },
    get mouseArea() { return this.getMouseArea(); },
    
    set touchArea( value ) { this.setTouchArea( value ); },
    get touchArea() { return this.getTouchArea(); },
    
    set visible( value ) { this.setVisible( value ); },
    get visible() { return this.isVisible(); },
    
    set opacity( value ) { this.setOpacity( value ); },
    get opacity() { return this.getOpacity(); },
    
    set pickable( value ) { this.setPickable( value ); },
    get pickable() { return this.isPickable(); },
    
    set transform( value ) { this.setTransform( value ); },
    get transform() { return this.getTransform(); },
    
    set matrix( value ) { this.setMatrix( value ); },
    get matrix() { return this.getMatrix(); },
    
    set translation( value ) { this.setTranslation( value ); },
    get translation() { return this.getTranslation(); },
    
    set rotation( value ) { this.setRotation( value ); },
    get rotation() { return this.getRotation(); },
    
    set x( value ) { this.setX( value ); },
    get x() { return this.getX(); },
    
    set y( value ) { this.setY( value ); },
    get y() { return this.getY(); },
    
    set left( value ) { this.setLeft( value ); },
    get left() { return this.getLeft(); },
    
    set right( value ) { this.setRight( value ); },
    get right() { return this.getRight(); },
    
    set top( value ) { this.setTop( value ); },
    get top() { return this.getTop(); },
    
    set bottom( value ) { this.setBottom( value ); },
    get bottom() { return this.getBottom(); },
    
    set center( value ) { this.setCenter( value ); },
    get center() { return this.getCenter(); },
    
    set centerX( value ) { this.setCenterX( value ); },
    get centerX() { return this.getCenterX(); },
    
    set centerY( value ) { this.setCenterY( value ); },
    get centerY() { return this.getCenterY(); },
    
    set children( value ) { this.setChildren( value ); },
    get children() { return this.getChildren(); },
    
    get parents() { return this.getParents(); },
    
    get width() { return this.getWidth(); },
    get height() { return this.getHeight(); },
    get bounds() { return this.getBounds(); },
    get selfBounds() { return this.getSelfBounds(); },
    get childBounds() { return this.getChildBounds(); },
    get globalBounds() { return this.getGlobalBounds(); },
    get visibleBounds() { return this.getVisibleBounds(); },
    get id() { return this.getId(); },
    get instances() { return this.getInstances(); },
    
    mutate: function( options ) {
      if ( !options ) {
        return this;
      }
      
      var node = this;
      
      _.each( this._mutatorKeys, function( key ) {
        if ( options[key] !== undefined ) {
          var descriptor = Object.getOwnPropertyDescriptor( Node.prototype, key );
          
          // if the key refers to a function that is not ES5 writable, it will execute that function with the single argument
          if ( descriptor && typeof descriptor.value === 'function' ) {
            node[key]( options[key] );
          } else {
            node[key] = options[key];
          }
        }
      } );
      
      return this; // allow chaining
    },
    
    toString: function( spaces, includeChildren ) {
      spaces = spaces || '';
      var props = this.getPropString( spaces + '  ', includeChildren === undefined ? true : includeChildren );
      return spaces + this.getBasicConstructor( props ? ( '\n' + props + '\n' + spaces ) : '' );
    },
    
    getBasicConstructor: function( propLines ) {
      return 'new scenery.Node( {' + propLines + '} )';
    },
    
    getPropString: function( spaces, includeChildren ) {
      var self = this;
      
      var result = '';
      function addProp( key, value, nowrap ) {
        if ( result ) {
          result += ',\n';
        }
        if ( !nowrap && typeof value === 'string' ) {
          result += spaces + key + ': \'' + value + '\'';
        } else {
          result += spaces + key + ': ' + value;
        }
      }
      
      if ( this._children.length && includeChildren ) {
        var childString = '';
        _.each( this._children, function( child ) {
          if ( childString ) {
            childString += ',\n';
          }
          childString += child.toString( spaces + '  ' );
        } );
        addProp( 'children', '[\n' + childString + '\n' + spaces + ']', true );
      }
      
      // direct copy props
      if ( this.cursor ) { addProp( 'cursor', this.cursor ); }
      if ( !this.visible ) { addProp( 'visible', this.visible ); }
      if ( !this.pickable ) { addProp( 'pickable', this.pickable ); }
      if ( this.opacity !== 1 ) { addProp( 'opacity', this.opacity ); }
      
      if ( !this.transform.isIdentity() ) {
        var m = this.transform.getMatrix();
        addProp( 'matrix', 'new dot.Matrix3( ' + m.m00() + ', ' + m.m01() + ', ' + m.m02() + ', ' +
                                                 m.m10() + ', ' + m.m11() + ', ' + m.m12() + ', ' +
                                                 m.m20() + ', ' + m.m21() + ', ' + m.m22() + ' )', true );
      }
      
      if ( this.renderer ) {
        addProp( 'renderer', this.renderer.name );
        if ( this.rendererOptions ) {
          // addProp( 'rendererOptions', JSON.stringify( this.rendererOptions ), true );
        }
      }
      
      if ( this._layerSplitBefore ) {
        addProp( 'layerSplitBefore', true );
      }
      
      if ( this._layerSplitAfter ) {
        addProp( 'layerSplitAfter', true );
      }
      
      return result;
    }
  };
  
  /*
   * This is an array of property (setter) names for Node.mutate(), which are also used when creating nodes with parameter objects.
   *
   * E.g. new scenery.Node( { x: 5, rotation: 20 } ) will create a Path, and apply setters in the order below (node.x = 5; node.rotation = 20)
   *
   * The order below is important! Don't change this without knowing the implications.
   * NOTE: translation-based mutators come before rotation/scale, since typically we think of their operations occuring "after" the rotation / scaling
   * NOTE: left/right/top/bottom/centerX/centerY are at the end, since they rely potentially on rotation / scaling changes of bounds that may happen beforehand
   * TODO: using more than one of {translation,x,left,right,centerX} or {translation,y,top,bottom,centerY} should be considered an error
   * TODO: move fill / stroke setting to mixins
   */
  Node.prototype._mutatorKeys = [ 'children', 'cursor', 'visible', 'pickable', 'opacity', 'matrix', 'translation', 'x', 'y', 'rotation', 'scale',
                                  'left', 'right', 'top', 'bottom', 'center', 'centerX', 'centerY', 'renderer', 'rendererOptions',
                                  'layerSplit', 'layerSplitBefore', 'layerSplitAfter', 'mouseArea', 'touchArea' ];
  
  Node.prototype._supportedRenderers = [];
  
  Node.prototype.layerStrategy = LayerStrategy;
  
  // mix-in the events for Node
  /* jshint -W064 */
  NodeEvents( Node );
  
  return Node;
} );

// Copyright 2002-2013, University of Colorado

/**
 * A description of layer settings and the ability to create a layer with those settings.
 * Used internally for the layer building process.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/LayerType',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  scenery.LayerType = function LayerType( Constructor, name, renderer, args ) {
    this.Constructor = Constructor;
    this.name = name;
    this.renderer = renderer;
    this.args = args;
  };
  var LayerType = scenery.LayerType;
  
  LayerType.prototype = {
    constructor: LayerType,
    
    supportsRenderer: function( renderer ) {
      return this.renderer === renderer;
    },
    
    supportsNode: function( node ) {
      var supportedRenderers = node._supportedRenderers;
      var i = supportedRenderers.length;
      while ( i-- ) {
        if ( this.supportsRenderer( supportedRenderers[i] ) ) {
          return true;
        }
      }
      return false;
    },
    
    createLayer: function( args ) {
      var Constructor = this.Constructor;
      return new Constructor( _.extend( {}, args, this.args ) ); // allow overriding certain arguments if necessary by the LayerType
    }
  };
  
  return LayerType;
} );



// Copyright 2002-2013, University of Colorado

/**
 * Represents a trail (path in the graph) from a 'root' node down to a descendant node.
 * In a DAG, or with different views, there can be more than one trail up from a node,
 * even to the same root node!
 *
 * It has an array of nodes, in order from the 'root' down to the last node,
 * a length, and an array of indices such that node_i.children[index_i] === node_{i+1}.
 *
 * The indices can sometimes become stale when nodes are added and removed, so Trails
 * can have their indices updated with reindex(). It's designed to be as fast as possible
 * on Trails that are already indexed accurately.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/Trail',['require','DOT/Matrix3','DOT/Transform3','SCENERY/scenery','SCENERY/nodes/Node'], function( require ) {
  
  
  var Matrix3 = require( 'DOT/Matrix3' );
  var Transform3 = require( 'DOT/Transform3' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  require( 'SCENERY/nodes/Node' );
  // require( 'SCENERY/util/TrailPointer' );
  
  scenery.Trail = function Trail( nodes ) {
    /*
     * Controls the immutability of the trail.
     * If set to true, add/remove descendant/ancestor should fail if assertions are enabled
     * Use setImmutable() or setMutable() to signal a specific type of protection, so it cannot be changed later
     */
    if ( sceneryAssert ) {
      // only do this if assertions are enabled, otherwise we won't access it at all
      this.immutable = undefined;
    }
    
    if ( nodes instanceof Trail ) {
      // copy constructor (takes advantage of already built index information)
      var otherTrail = nodes;
      
      this.nodes = otherTrail.nodes.slice( 0 );
      this.length = otherTrail.length;
      this.uniqueId = otherTrail.uniqueId;
      this.indices = otherTrail.indices.slice( 0 );
      return;
    }
    
    this.nodes = [];
    this.length = 0;
    this.uniqueId = '';
    
    // indices[x] stores the index of nodes[x] in nodes[x-1]'s children
    this.indices = [];
    
    var trail = this;
    if ( nodes ) {
      if ( nodes instanceof scenery.Node ) {
        var node = nodes;
        
        // add just a single node in
        trail.addDescendant( node );
      } else {
        // process it as an array
        var len = nodes.length;
        for ( var i = 0; i < len; i++ ) {
          trail.addDescendant( nodes[i] );
        }
      }
    }
    
    phetAllocation && phetAllocation( 'Trail' );
  };
  var Trail = scenery.Trail;
  
  Trail.prototype = {
    constructor: Trail,
    
    copy: function() {
      return new Trail( this );
    },
    
    // convenience function to determine whether this trail will render something
    isPainted: function() {
      return this.lastNode().isPainted();
    },
    
    // this trail is visible only if all nodes on it are marked as visible
    isVisible: function() {
      var i = this.nodes.length;
      while ( i-- ) {
        if ( !this.nodes[i].isVisible() ) {
          return false;
        }
      }
      return true;
    },
    
    getOpacity: function() {
      var opacity = 1;
      var i = this.nodes.length;
      while ( i-- ) {
        opacity *= this.nodes[i].getOpacity();
      }
      return opacity;
    },
    
    get: function( index ) {
      if ( index >= 0 ) {
        return this.nodes[index];
      } else {
        // negative index goes from the end of the array
        return this.nodes[this.nodes.length + index];
      }
    },
    
    slice: function( startIndex, endIndex ) {
      return new Trail( this.nodes.slice( startIndex, endIndex ) );
    },
    
    subtrailTo: function( node, excludeNode ) {
      return this.slice( 0, _.indexOf( this.nodes, node ) + ( excludeNode ? 0 : 1 ) );
    },
    
    isEmpty: function() {
      return this.nodes.length === 0;
    },
    
    getInstance: function() {
      return this.lastNode().getInstanceFromTrail( this );
    },
    
    // from local to global
    getMatrix: function() {
      // TODO: performance: can we cache this ever? would need the scene to not really change in between
      // this matrix will be modified in place, so always start fresh
      var matrix = new Matrix3();
      
      // from the root up
      var nodes = this.nodes;
      var length = nodes.length;
      for ( var i = 0; i < length; i++ ) {
        matrix.multiplyMatrix( nodes[i]._transform.getMatrix() );
      }
      return matrix;
    },
    
    // from parent to global
    getParentMatrix: function() {
      // this matrix will be modified in place, so always start fresh
      var matrix = new Matrix3();
      
      // from the root up
      var nodes = this.nodes;
      var length = nodes.length;
      for ( var i = 0; i < length - 1; i++ ) {
        matrix.multiplyMatrix( nodes[i]._transform.getMatrix() );
      }
      return matrix;
    },
    
    // from local to global
    getTransform: function() {
      return new Transform3( this.getMatrix() );
    },
    
    // from parent to global
    getParentTransform: function() {
      return new Transform3( this.getParentMatrix() );
    },
    
    addAncestor: function( node, index ) {
      sceneryAssert && sceneryAssert( !this.immutable, 'cannot modify an immutable Trail with addAncestor' );
      sceneryAssert && sceneryAssert( node, 'cannot add falsy value to a Trail' );
      
      
      if ( this.nodes.length ) {
        var oldRoot = this.nodes[0];
        this.indices.unshift( index === undefined ? _.indexOf( node._children, oldRoot ) : index );
      }
      this.nodes.unshift( node );
      
      this.length++;
      // accelerated version of this.updateUniqueId()
      this.uniqueId = ( this.uniqueId ? node._id + '-' + this.uniqueId : node._id + '' );
      return this;
    },
    
    removeAncestor: function() {
      sceneryAssert && sceneryAssert( !this.immutable, 'cannot modify an immutable Trail with removeAncestor' );
      sceneryAssert && sceneryAssert( this.length > 0, 'cannot remove a Node from an empty trail' );
      
      this.nodes.shift();
      if ( this.indices.length ) {
        this.indices.shift();
      }
      
      this.length--;
      this.updateUniqueId();
      return this;
    },
    
    addDescendant: function( node, index ) {
      sceneryAssert && sceneryAssert( !this.immutable, 'cannot modify an immutable Trail with addDescendant' );
      sceneryAssert && sceneryAssert( node, 'cannot add falsy value to a Trail' );
      
      
      if ( this.nodes.length ) {
        var parent = this.lastNode();
        this.indices.push( index === undefined ? _.indexOf( parent._children, node ) : index );
      }
      this.nodes.push( node );
      
      this.length++;
      // accelerated version of this.updateUniqueId()
      this.uniqueId = ( this.uniqueId ? this.uniqueId + '-' + node._id : node._id + '' );
      return this;
    },
    
    removeDescendant: function() {
      sceneryAssert && sceneryAssert( !this.immutable, 'cannot modify an immutable Trail with removeDescendant' );
      sceneryAssert && sceneryAssert( this.length > 0, 'cannot remove a Node from an empty trail' );
      
      this.nodes.pop();
      if ( this.indices.length ) {
        this.indices.pop();
      }
      
      this.length--;
      this.updateUniqueId();
      return this;
    },
    
    // refreshes the internal index references (important if any children arrays were modified!)
    reindex: function() {
      var length = this.length;
      for ( var i = 1; i < length; i++ ) {
        // only replace indices where they have changed (this was a performance hotspot)
        var currentIndex = this.indices[i-1];
        var baseNode = this.nodes[i-1];
        
        if ( baseNode._children[currentIndex] !== this.nodes[i] ) {
          this.indices[i-1] = _.indexOf( baseNode._children, this.nodes[i] );
        }
      }
    },
    
    setImmutable: function() {
      // if assertions are disabled, we hope this is inlined as a no-op
      if ( sceneryAssert ) {
        sceneryAssert( this.immutable !== false, 'A trail cannot be made immutable after being flagged as mutable' );
        this.immutable = true;
      }
      
      // TODO: consider setting mutators to null here instead of the function call check (for performance, and profile the differences)
      
      return this; // allow chaining
    },
    
    setMutable: function() {
      // if assertions are disabled, we hope this is inlined as a no-op
      if ( sceneryAssert ) {
        sceneryAssert( this.immutable !== true, 'A trail cannot be made mutable after being flagged as immutable' );
        this.immutable = false;
      }
      
      return this; // allow chaining
    },
    
    areIndicesValid: function() {
      for ( var i = 1; i < this.length; i++ ) {
        var currentIndex = this.indices[i-1];
        if ( this.nodes[i-1]._children[currentIndex] !== this.nodes[i] ) {
          return false;
        }
      }
      return true;
    },
    
    equals: function( other ) {
      if ( this.length !== other.length ) {
        return false;
      }
      
      for ( var i = 0; i < this.nodes.length; i++ ) {
        if ( this.nodes[i] !== other.nodes[i] ) {
          return false;
        }
      }
      
      return true;
    },
    
    // returns a new Trail from the root up to the parameter node.
    upToNode: function( node ) {
      var nodeIndex = _.indexOf( this.nodes, node );
      sceneryAssert && sceneryAssert( nodeIndex >= 0, 'Trail does not contain the node' );
      return this.slice( 0, _.indexOf( this.nodes, node ) + 1 );
    },
    
    // whether this trail contains the complete 'other' trail, but with added descendants afterwards
    isExtensionOf: function( other, allowSameTrail ) {
      sceneryAssertExtra && sceneryAssertExtra( this.areIndicesValid(), 'Trail.compare this.areIndicesValid() failed' );
      sceneryAssertExtra && sceneryAssertExtra( other.areIndicesValid(), 'Trail.compare other.areIndicesValid() failed' );
      
      if ( this.length <= other.length - ( allowSameTrail ? 1 : 0 ) ) {
        return false;
      }
      
      for ( var i = 0; i < other.nodes.length; i++ ) {
        if ( this.nodes[i] !== other.nodes[i] ) {
          return false;
        }
      }
      
      return true;
    },
    
    // a transform from our local coordinate frame to the other trail's local coordinate frame
    getTransformTo: function( otherTrail ) {
      return new Transform3( this.getMatrixTo( otherTrail ) );
    },
    
    // returns a matrix that transforms a point in our last node's local coordinate frame to the other trail's last node's local coordinate frame
    getMatrixTo: function( otherTrail ) {
      this.reindex();
      otherTrail.reindex();
      
      var branchIndex = this.getBranchIndexTo( otherTrail );
      var idx;
      
      var matrix = Matrix3.IDENTITY;
      
      // walk our transform down, prepending
      for ( idx = this.length-1; idx >= branchIndex; idx-- ) {
        matrix = this.nodes[idx].getTransform().getMatrix().timesMatrix( matrix );
      }
      
      // walk our transform up, prepending inverses
      for ( idx = branchIndex; idx < otherTrail.length; idx++ ) {
        matrix = otherTrail.nodes[idx].getTransform().getInverse().timesMatrix( matrix );
      }
      
      return matrix;
    },
    
    // the first index that is different between this trail and the other trail
    getBranchIndexTo: function( otherTrail ) {
      sceneryAssert && sceneryAssert( this.nodes[0] === otherTrail.nodes[0], 'To get a branch index, the trails must have the same root' );
      
      var branchIndex;
      
      for ( branchIndex = 0; branchIndex < Math.min( this.length, otherTrail.length ); branchIndex++ ) {
        if ( this.nodes[branchIndex] !== otherTrail.nodes[branchIndex] ) {
          break;
        }
      }
      
      return branchIndex;
    },
    
    // TODO: phase out in favor of get()
    nodeFromTop: function( offset ) {
      return this.nodes[this.length - 1 - offset];
    },
    
    lastNode: function() {
      return this.nodeFromTop( 0 );
    },
    
    rootNode: function() {
      return this.nodes[0];
    },
    
    // returns the previous graph trail in the order of self-rendering
    previous: function() {
      if ( this.nodes.length <= 1 ) {
        return null;
      }
      
      var top = this.nodeFromTop( 0 );
      var parent = this.nodeFromTop( 1 );
      
      var parentIndex = _.indexOf( parent._children, top );
      sceneryAssert && sceneryAssert( parentIndex !== -1 );
      var arr = this.nodes.slice( 0, this.nodes.length - 1 );
      if ( parentIndex === 0 ) {
        // we were the first child, so give it the trail to the parent
        return new Trail( arr );
      } else {
        // previous child
        arr.push( parent._children[parentIndex-1] );
        
        // and find its last terminal
        while( arr[arr.length-1]._children.length !== 0 ) {
          var last = arr[arr.length-1];
          arr.push( last._children[last._children.length-1] );
        }
        
        return new Trail( arr );
      }
    },
    
    // like previous(), but keeps moving back until the trail goes to a node with isPainted() === true
    previousPainted: function() {
      var result = this.previous();
      while ( result && !result.isPainted() ) {
        result = result.previous();
      }
      return result;
    },
    
    // in the order of self-rendering
    next: function() {
      var arr = this.nodes.slice( 0 );
      
      var top = this.nodeFromTop( 0 );
      if ( top._children.length > 0 ) {
        // if we have children, return the first child
        arr.push( top._children[0] );
        return new Trail( arr );
      } else {
        // walk down and attempt to find the next parent
        var depth = this.nodes.length - 1;
        
        while ( depth > 0 ) {
          var node = this.nodes[depth];
          var parent = this.nodes[depth-1];
          
          arr.pop(); // take off the node so we can add the next sibling if it exists
          
          var index = _.indexOf( parent._children, node );
          if ( index !== parent._children.length - 1 ) {
            // there is another (later) sibling. use that!
            arr.push( parent._children[index+1] );
            return new Trail( arr );
          } else {
            depth--;
          }
        }
        
        // if we didn't reach a later sibling by now, it doesn't exist
        return null;
      }
    },
    
    // like next(), but keeps moving back until the trail goes to a node with isPainted() === true
    nextPainted: function() {
      var result = this.next();
      while ( result && !result.isPainted() ) {
        result = result.next();
      }
      return result;
    },
    
    // calls callback( trail ) for this trail, and each descendant trail
    eachTrailUnder: function( callback ) {
      // TODO: performance: should be optimized to be much faster, since we don't have to deal with the before/after
      new scenery.TrailPointer( this, true ).eachTrailBetween( new scenery.TrailPointer( this, false ), callback );
    },
    
    /*
     * Standard Java-style compare. -1 means this trail is before (under) the other trail, 0 means equal, and 1 means this trail is
     * after (on top of) the other trail.
     * A shorter subtrail will compare as -1.
     *
     * Assumes that the Trails are properly indexed. If not, please reindex them!
     *
     * Comparison is for the rendering order, so an ancestor is 'before' a descendant
     */
    compare: function( other ) {
      sceneryAssert && sceneryAssert( !this.isEmpty(), 'cannot compare with an empty trail' );
      sceneryAssert && sceneryAssert( !other.isEmpty(), 'cannot compare with an empty trail' );
      sceneryAssert && sceneryAssert( this.nodes[0] === other.nodes[0], 'for Trail comparison, trails must have the same root node' );
      sceneryAssertExtra && sceneryAssertExtra( this.areIndicesValid(), 'Trail.compare this.areIndicesValid() failed on ' + this.toString() );
      sceneryAssertExtra && sceneryAssertExtra( other.areIndicesValid(), 'Trail.compare other.areIndicesValid() failed on ' + other.toString() );
      
      var minNodeIndex = Math.min( this.indices.length, other.indices.length );
      for ( var i = 0; i < minNodeIndex; i++ ) {
        if ( this.indices[i] !== other.indices[i] ) {
          if ( this.indices[i] < other.indices[i] ) {
            return -1;
          } else {
            return 1;
          }
        }
      }
      
      // we scanned through and no nodes were different (one is a subtrail of the other)
      if ( this.nodes.length < other.nodes.length ) {
        return -1;
      } else if ( this.nodes.length > other.nodes.length ) {
        return 1;
      } else {
        return 0;
      }
    },
    
    isBefore: function( other ) {
      return this.compare( other ) === -1;
    },
    
    isAfter: function( other ) {
      return this.compare( other ) === 1;
    },
    
    localToGlobalPoint: function( point ) {
      // TODO: performance: multiple timesVector2 calls up the chain is probably faster
      return this.getMatrix().timesVector2( point );
    },
    
    localToGlobalBounds: function( bounds ) {
      return bounds.transformed( this.getMatrix() );
    },
    
    globalToLocalPoint: function( point ) {
      return this.getTransform().inversePosition2( point );
    },
    
    globalToLocalBounds: function( bounds ) {
      return this.getTransform().inverseBounds2( bounds );
    },
    
    parentToGlobalPoint: function( point ) {
      // TODO: performance: multiple timesVector2 calls up the chain is probably faster
      return this.getParentMatrix().timesVector2( point );
    },
    
    parentToGlobalBounds: function( bounds ) {
      return bounds.transformed( this.getParentMatrix() );
    },
    
    globalToParentPoint: function( point ) {
      return this.getParentTransform().inversePosition2( point );
    },
    
    globalToParentBounds: function( bounds ) {
      return this.getParentTransform().inverseBounds2( bounds );
    },
    
    updateUniqueId: function() {
      // string concatenation is faster, see http://jsperf.com/string-concat-vs-joins
      var result = '';
      var len = this.nodes.length;
      if ( len > 0 ) {
        result += this.nodes[0]._id;
      }
      for ( var i = 1; i < len; i++ ) {
        result += '-' + this.nodes[i]._id;
      }
      this.uniqueId = result;
      // this.uniqueId = _.map( this.nodes, function( node ) { return node.getId(); } ).join( '-' );
    },
    
    // concatenates the unique IDs of nodes in the trail, so that we can do id-based lookups
    getUniqueId: function() {
      // sanity checks
      if ( sceneryAssert ) {
        var oldUniqueId = this.uniqueId;
        this.updateUniqueId();
        sceneryAssert( oldUniqueId === this.uniqueId );
      }
      return this.uniqueId;
    },
    
    toString: function() {
      this.reindex();
      if ( !this.length ) {
        return 'Empty Trail';
      }
      return '[Trail ' + this.indices.join( '.' ) + ' ' + this.getUniqueId() + ']';
    }
  };
  
  // like eachTrailBetween, but only fires for painted trails
  Trail.eachPaintedTrailBetween = function( a, b, callback, excludeEndTrails, scene ) {
    Trail.eachTrailBetween( a, b, function( trail ) {
      if ( trail && trail.isPainted() ) {
        callback( trail );
      }
    }, excludeEndTrails, scene );
  };
  
  // global way of iterating across trails
  Trail.eachTrailBetween = function( a, b, callback, excludeEndTrails, scene ) {
    var aPointer = a ? new scenery.TrailPointer( a.copy(), true ) : new scenery.TrailPointer( new scenery.Trail( scene ), true );
    var bPointer = b ? new scenery.TrailPointer( b.copy(), true ) : new scenery.TrailPointer( new scenery.Trail( scene ), false );
    
    // if we are excluding endpoints, just bump the pointers towards each other by one step
    if ( excludeEndTrails ) {
      aPointer.nestedForwards();
      bPointer.nestedBackwards();
      
      // they were adjacent, so no callbacks will be executed
      if ( aPointer.compareNested( bPointer ) === 1 ) {
        return;
      }
    }
    
    aPointer.depthFirstUntil( bPointer, function( pointer ) {
      if ( pointer.isBefore ) {
        callback( pointer.trail );
      }
    }, false );
  };
  
  return Trail;
} );



// Copyright 2002-2013, University of Colorado

/**
 * Base code for layers that helps with shared layer functions
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/Layer',['require','DOT/Bounds2','DOT/Transform3','SCENERY/scenery','SCENERY/util/Trail'], function( require ) {
  
  
  var Bounds2 = require( 'DOT/Bounds2' );
  var Transform3 = require( 'DOT/Transform3' );
  
  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/util/Trail' );
  
  var globalIdCounter = 1;
  
  /*
   * Required arguments:
   * $main     - the jQuery-wrapped container for the scene
   * scene     - the scene itself
   * baseNode  - the base node for this layer
   */
  scenery.Layer = function Layer( args ) {
    
    // assign a unique ID to this layer
    this._id = globalIdCounter++;
    
    this.$main = args.$main;
    this.scene = args.scene;
    this.baseNode = args.baseNode;
    
    // TODO: cleanup of flags!
    this.usesPartialCSSTransforms = args.cssTranslation || args.cssRotation || args.cssScale;
    this.cssTranslation = args.cssTranslation; // CSS for the translation
    this.cssRotation = args.cssRotation;       // CSS for the rotation
    this.cssScale = args.cssScale;             // CSS for the scaling
    this.cssTransform = args.cssTransform;     // CSS for the entire base node (will ignore other partial transforms)
    sceneryAssert && sceneryAssert( !( this.usesPartialCSSTransforms && this.cssTransform ), 'Do not specify both partial and complete CSS transform arguments.' );
    
    // initialize to fully dirty so we draw everything the first time
    // bounds in global coordinate frame
    this.dirtyBounds = Bounds2.EVERYTHING;
    
    this.setStartBoundary( args.startBoundary );
    this.setEndBoundary( args.endBoundary );
    
    // set baseTrail from the scene to our baseNode
    if ( this.baseNode === this.scene ) {
      this.baseTrail = new scenery.Trail( this.scene );
    } else {
      this.baseTrail = this.startPaintedTrail.upToNode( this.baseNode );
      sceneryAssert && sceneryAssert( this.baseTrail.lastNode() === this.baseNode );
    }
    
    // we reference all painted trails in an unordered way
    this._layerTrails = []; // TODO: performance: remove layerTrails if possible!
    this._instanceCount = 0; // track how many instances we are tracking (updated in stitching by instances)
    
    var layer = this;
    
    // whenever the base node's children or self change bounds, signal this. we want to explicitly ignore the base node's main bounds for
    // CSS transforms, since the self / children bounds may not have changed
    this.baseNodeBoundsListener = function( bounds ) {
      layer.baseNodeInternalBoundsChange(); // TODO: verify that this is working as expected
    };
    this.baseNode.addEventListener( 'selfBounds', this.baseNodeBoundsListener );
    this.baseNode.addEventListener( 'childBounds', this.baseNodeBoundsListener );
    
    this.fitToBounds = this.usesPartialCSSTransforms || this.cssTransform;
    sceneryAssert && sceneryAssert( this.fitToBounds || this.baseNode === this.scene, 'If the baseNode is not the scene, we need to fit the bounds' );
    
    // used for CSS transforms where we need to transform our base node's bounds into the (0,0,w,h) bounds range
    this.baseNodeTransform = new Transform3();
    //this.baseNodeInteralBounds = Bounds2.NOTHING; // stores the bounds transformed into (0,0,w,h)
    
    this.disposed = false; // track whether we have been disposed or not
  };
  var Layer = scenery.Layer;
  
  Layer.prototype = {
    constructor: Layer,
    
    setStartBoundary: function( boundary ) {
      // console.log( 'setting start boundary on layer ' + this.getId() + ': ' + boundary.toString() );
      this.startBoundary = boundary;
      
      // TODO: deprecate these, use boundary references instead? or boundary convenience functions
      this.startPaintedTrail = this.startBoundary.nextPaintedTrail;
      
      // set immutability guarantees
      this.startPaintedTrail.setImmutable();
    },
    
    setEndBoundary: function( boundary ) {
      // console.log( 'setting end boundary on layer ' + this.getId() + ': ' + boundary.toString() );
      this.endBoundary = boundary;
      
      // TODO: deprecate these, use boundary references instead? or boundary convenience functions
      this.endPaintedTrail = this.endBoundary.previousPaintedTrail;
      
      // set immutability guarantees
      this.endPaintedTrail.setImmutable();
    },
    
    toString: function() {
      return this.getName() + ' ' + ( this.startPaintedTrail ? this.startPaintedTrail.toString() : '!' ) + ' => ' + ( this.endPaintedTrail ? this.endPaintedTrail.toString() : '!' );
    },
    
    getId: function() {
      return this._id;
    },
    get id() { return this._id; }, // ES5 version
    
    // painted trails associated with the layer, NOT necessarily in order
    getLayerTrails: function() {
      return this._layerTrails.slice( 0 );
    },
    
    getPaintedTrailCount: function() {
      return this._layerTrails.length;
    },
    
    /*---------------------------------------------------------------------------*
    * Abstract
    *----------------------------------------------------------------------------*/
    
    render: function( state ) {
      throw new Error( 'Layer.render unimplemented' );
    },
    
    // TODO: consider a stack-based model for transforms?
    // TODO: is this necessary? verify with the render state
    applyTransformationMatrix: function( matrix ) {
      throw new Error( 'Layer.applyTransformationMatrix unimplemented' );
    },
    
    // adds a trail (with the last node) to the layer
    addInstance: function( instance ) {
      var trail = instance.trail;
      
      if ( sceneryAssert ) {
        _.each( this._layerTrails, function( otherTrail ) {
          sceneryAssert( !trail.equals( otherTrail ), 'trail in addInstance should not already exist in a layer' );
        } );
      }
      
      // TODO: sync this with DOMLayer's implementation
      this._layerTrails.push( trail );
      trail.setImmutable(); // don't allow this Trail to be changed
    },
    
    // removes a trail (with the last node) to the layer
    removeInstance: function( instance ) {
      // TODO: sync this with DOMLayer's implementation
      var i;
      for ( i = 0; i < this._layerTrails.length; i++ ) {
        this._layerTrails[i].reindex();
        if ( this._layerTrails[i].compare( instance.trail ) === 0 ) {
          break;
        }
      }
      sceneryAssert && sceneryAssert( i < this._layerTrails.length );
      
      this._layerTrails.splice( i, 1 );
    },
    
    // returns next zIndex in place. allows layers to take up more than one single zIndex
    reindex: function( zIndex ) {
      this.startBoundary.reindex();
      this.endBoundary.reindex();
    },
    
    pushClipShape: function( shape ) {
      throw new Error( 'Layer.pushClipShape unimplemented' );
    },
    
    popClipShape: function() {
      throw new Error( 'Layer.popClipShape unimplemented' );
    },
    
    renderToCanvas: function( canvas, context, delayCounts ) {
      throw new Error( 'Layer.renderToCanvas unimplemented' );
    },
    
    dispose: function() {
      sceneryAssert && sceneryAssert( !this.disposed, 'Layer has already been disposed!' );
      
      this.disposed = true;
      
      // clean up listeners
      this.baseNode.removeEventListener( 'selfBounds', this.baseNodeBoundsListener );
      this.baseNode.removeEventListener( 'childBounds', this.baseNodeBoundsListener );
    },
    
    getName: function() {
      throw new Error( 'Layer.getName unimplemented' );
    },
    
    // called when the base node's "internal" (self or child) bounds change, but not when it is just from the base node's own transform changing
    baseNodeInternalBoundsChange: function() {
      // no error, many times this doesn't need to be handled
    }
    
  };
  
  Layer.cssTransformPadding = 3;
  
  return Layer;
} );



// Copyright 2002-2013, University of Colorado

/**
 * Wraps the context and contains a reference to the canvas, so that we can absorb unnecessary state changes,
 * and possibly combine certain fill operations.
 *
 * TODO: performance analysis, possibly axe this and use direct modification.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/CanvasContextWrapper',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  scenery.CanvasContextWrapper = function CanvasContextWrapper( canvas, context ) {
    this.canvas = canvas;
    this.context = context;
    
    this.resetStyles();
    
    phetAllocation && phetAllocation( 'CanvasContextWrapper' );
  };
  var CanvasContextWrapper = scenery.CanvasContextWrapper;
  
  CanvasContextWrapper.prototype = {
    constructor: CanvasContextWrapper,
    
    // set local styles to undefined, so that they will be invalidated later
    resetStyles: function() {
      this.fillStyle = undefined; // null
      this.strokeStyle = undefined; // null
      this.lineWidth = undefined; // 1
      this.lineCap = undefined; // 'butt'
      this.lineJoin = undefined; // 'miter'
      this.lineDash = undefined; // []
      this.lineDashOffset = undefined; // 0
      this.miterLimit = undefined; // 10
      
      this.font = undefined; // '10px sans-serif'
      this.direction = undefined; // 'inherit'
    },

    /**
     * Sets a (possibly) new width and height, and clears the canvas.
     * @param width
     * @param height
     */
    setDimensions: function( width, height ) {

      //Don't guard against width and height, because we need to clear the canvas.
      //TODO: Is it expensive to clear by setting both the width and the height?  Maybe we just need to set the width to clear it.
      this.canvas.width = width;
      this.canvas.height = height;

      // assume all persistent data could have changed
      this.resetStyles();
    },
    
    setFillStyle: function( style ) {
      if ( this.fillStyle !== style ) {
        this.fillStyle = style;
        
        // allow gradients / patterns
        this.context.fillStyle = ( style && style.getCanvasStyle ) ? style.getCanvasStyle() : style;
      }
    },
    
    setStrokeStyle: function( style ) {
      if ( this.strokeStyle !== style ) {
        this.strokeStyle = style;
        
        // allow gradients / patterns
        this.context.strokeStyle = ( style && style.getCanvasStyle ) ? style.getCanvasStyle() : style;
      }
    },
    
    setLineWidth: function( width ) {
      if ( this.lineWidth !== width ) {
        this.lineWidth = width;
        this.context.lineWidth = width;
      }
    },
    
    setLineCap: function( cap ) {
      if ( this.lineCap !== cap ) {
        this.lineCap = cap;
        this.context.lineCap = cap;
      }
    },
    
    setLineJoin: function( join ) {
      if ( this.lineJoin !== join ) {
        this.lineJoin = join;
        this.context.lineJoin = join;
      }
    },
    
    setLineDash: function( dash ) {
      sceneryAssert && sceneryAssert( dash !== undefined, 'undefined line dash would cause hard-to-trace errors' );
      if ( this.lineDash !== dash ) {
        this.lineDash = dash;
        if ( this.context.setLineDash ) {
          this.context.setLineDash( dash === null ? [] : dash ); // see https://github.com/phetsims/scenery/issues/101 for null line-dash workaround
        } else if ( this.context.mozDash !== undefined ) {
          this.context.mozDash = dash;
        } else if ( this.context.webkitLineDash !== undefined ) {
          this.context.webkitLineDash = dash ? dash : [];
        } else {
          // unsupported line dash! do... nothing?
        }
      }
    },
    
    setLineDashOffset: function( lineDashOffset ) {
      if ( this.lineDashOffset !== lineDashOffset ) {
        this.lineDashOffset = lineDashOffset;
        if ( this.context.lineDashOffset !== undefined ) {
          this.context.lineDashOffset = lineDashOffset;
        } else if ( this.context.webkitLineDashOffset !== undefined ) {
          this.context.webkitLineDashOffset = lineDashOffset;
        } else {
          // unsupported line dash! do... nothing?
        }
      }
    },
    
    setFont: function( font ) {
      if ( this.font !== font ) {
        this.font = font;
        this.context.font = font;
      }
    },
    
    setDirection: function( direction ) {
      if ( this.direction !== direction ) {
        this.direction = direction;
        this.context.direction = direction;
      }
    }
  };
  
  return CanvasContextWrapper;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Points to a specific node (with a trail), and whether it is conceptually before or after the node.
 *
 * There are two orderings:
 * - rendering order: the order that node selves would be rendered, matching the Trail implicit order
 * - nesting order:   the order in depth first with entering a node being "before" and exiting a node being "after"
 *
 * TODO: more seamless handling of the orders. or just exclusively use the nesting order
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/TrailPointer',['require','SCENERY/scenery','SCENERY/util/Trail'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  require( 'SCENERY/util/Trail' );
  
  /*
   * isBefore: whether this points to before the node (and its children) have been rendered, or after
   */
  scenery.TrailPointer = function TrailPointer( trail, isBefore ) {
    sceneryAssert && sceneryAssert( trail instanceof scenery.Trail, 'trail is not a trail' );
    this.trail = trail;
    
    this.setBefore( isBefore );
    
    phetAllocation && phetAllocation( 'TrailPointer' );
  };
  var TrailPointer = scenery.TrailPointer;
  
  TrailPointer.prototype = {
    constructor: TrailPointer,
    
    copy: function() {
      return new TrailPointer( this.trail.copy(), this.isBefore );
    },
    
    setBefore: function( isBefore ) {
      this.isBefore = isBefore;
      this.isAfter = !isBefore;
    },
    
    // return the equivalent pointer that swaps before and after (may return null if it doesn't exist)
    getRenderSwappedPointer: function() {
      var newTrail = this.isBefore ? this.trail.previous() : this.trail.next();
      
      if ( newTrail === null ) {
        return null;
      } else {
        return new TrailPointer( newTrail, !this.isBefore );
      }
    },
    
    getRenderBeforePointer: function() {
      return this.isBefore ? this : this.getRenderSwappedPointer();
    },
    
    getRenderAfterPointer: function() {
      return this.isAfter ? this : this.getRenderSwappedPointer();
    },
    
    /*
     * In the render order, will return 0 if the pointers are equivalent, -1 if this pointer is before the
     * other pointer, and 1 if this pointer is after the other pointer.
     */
    compareRender: function( other ) {
      sceneryAssert && sceneryAssert( other !== null );
      
      var a = this.getRenderBeforePointer();
      var b = other.getRenderBeforePointer();
      
      if ( a !== null && b !== null ) {
        // normal (non-degenerate) case
        return a.trail.compare( b.trail );
      } else {
        // null "before" point is equivalent to the "after" pointer on the last rendered node.
        if ( a === b ) {
          return 0; // uniqueness guarantees they were the same
        } else {
          return a === null ? 1 : -1;
        }
      }
    },
    
    /*
     * Like compareRender, but for the nested (depth-first) order
     *
     * TODO: optimization?
     */
    compareNested: function( other ) {
      sceneryAssert && sceneryAssert( other );
      
      var comparison = this.trail.compare( other.trail );
      
      if ( comparison === 0 ) {
        // if trails are equal, just compare before/after
        if ( this.isBefore === other.isBefore ) {
          return 0;
        } else {
          return this.isBefore ? -1 : 1;
        }
      } else {
        // if one is an extension of the other, the shorter isBefore flag determines the order completely
        if ( this.trail.isExtensionOf( other.trail ) ) {
          return other.isBefore ? 1 : -1;
        } else if ( other.trail.isExtensionOf( this.trail ) ) {
          return this.isBefore ? -1 : 1;
        } else {
          // neither is a subtrail of the other, so a straight trail comparison should give the answer
          return comparison;
        }
      }
    },
    
    equalsRender: function( other ) {
      return this.compareRender( other ) === 0;
    },
    
    equalsNested: function( other ) {
      return this.compareNested( other ) === 0;
    },
    
    // will return false if this pointer has gone off of the beginning or end of the tree (will be marked with isAfter or isBefore though)
    hasTrail: function() {
      return !!this.trail;
    },
    
    // TODO: refactor with "Side"-like handling
    // moves this pointer forwards one step in the nested order
    nestedForwards: function() {
      if ( this.isBefore ) {
        if ( this.trail.lastNode()._children.length > 0 ) {
          // stay as before, just walk to the first child
          this.trail.addDescendant( this.trail.lastNode()._children[0], 0 );
        } else {
          // stay on the same node, but switch to after
          this.setBefore( false );
        }
      } else {
        if ( this.trail.indices.length === 0 ) {
          // nothing else to jump to below, so indicate the lack of existence
          this.trail = null;
          // stays isAfter
          return null;
        } else {
          var index = this.trail.indices[this.trail.indices.length - 1];
          this.trail.removeDescendant();
          
          if ( this.trail.lastNode()._children.length > index + 1 ) {
            // more siblings, switch to the beginning of the next one
            this.trail.addDescendant( this.trail.lastNode()._children[index+1], index + 1 );
            this.setBefore( true );
          } else {
            // no more siblings. exit on parent. nothing else needed since we're already isAfter
          }
        }
      }
      return this;
    },
    
    // moves this pointer backwards one step in the nested order
    nestedBackwards: function() {
      if ( this.isBefore ) {
        if ( this.trail.indices.length === 0 ) {
          // jumping off the front
          this.trail = null;
          // stays isBefore
          return null;
        } else {
          var index = this.trail.indices[this.trail.indices.length - 1];
          this.trail.removeDescendant();
          
          if ( index - 1 >= 0 ) {
            // more siblings, switch to the beginning of the previous one and switch to isAfter
            this.trail.addDescendant( this.trail.lastNode()._children[index-1], index - 1 );
            this.setBefore( false );
          } else {
            // no more siblings. enter on parent. nothing else needed since we're already isBefore
          }
        }
      } else {
        if ( this.trail.lastNode()._children.length > 0 ) {
          // stay isAfter, but walk to the last child
          var children = this.trail.lastNode()._children;
          this.trail.addDescendant( children[children.length-1], children.length - 1 );
        } else {
          // switch to isBefore, since this is a leaf node
          this.setBefore( true );
        }
      }
      return this;
    },
    
    // treats the pointer as render-ordered (includes the start pointer 'before' if applicable, excludes the end pointer 'before' if applicable
    eachNodeBetween: function( other, callback ) {
      this.eachTrailBetween( other, function( trail ) {
        callback( trail.lastNode() );
      } );
    },
    
    // treats the pointer as render-ordered (includes the start pointer 'before' if applicable, excludes the end pointer 'before' if applicable
    eachTrailBetween: function( other, callback ) {
      // this should trigger on all pointers that have the 'before' flag, except a pointer equal to 'other'.
      
      // since we exclude endpoints in the depthFirstUntil call, we need to fire this off first
      if ( this.isBefore ) {
        callback( this.trail );
      }
      
      this.depthFirstUntil( other, function( pointer ) {
        if ( pointer.isBefore ) {
          callback( pointer.trail );
        }
      }, true ); // exclude the endpoints so we can ignore the ending 'before' case
    },
    
    /*
     * Recursively (depth-first) iterates over all pointers between this pointer and 'other', calling
     * callback( pointer ) for each pointer. If excludeEndpoints is truthy, the callback will not be
     * called if pointer is equivalent to this pointer or 'other'.
     *
     * If the callback returns a truthy value, the subtree for the current pointer will be skipped
     * (applies only to before-pointers)
     */
    depthFirstUntil: function( other, callback, excludeEndpoints ) {
      // make sure this pointer is before the other, but allow start === end if we are not excluding endpoints
      sceneryAssert && sceneryAssert( this.compareNested( other ) <= ( excludeEndpoints ? -1 : 0 ), 'TrailPointer.depthFirstUntil pointers out of order, possibly in both meanings of the phrase!' );
      sceneryAssert && sceneryAssert( this.trail.rootNode() === other.trail.rootNode(), 'TrailPointer.depthFirstUntil takes pointers with the same root' );
      
      // sanity check TODO: remove later
      this.trail.reindex();
      other.trail.reindex();
      
      var pointer = this.copy();
      pointer.trail.setMutable(); // this trail will be modified in the iteration, so references to it may be modified
      
      var first = true;
      
      while ( !pointer.equalsNested( other ) ) {
        sceneryAssert && sceneryAssert( pointer.compareNested( other ) !== 1, 'skipped in depthFirstUntil' );
        var skipSubtree = false;
        
        if ( first ) {
          // start point
          if ( !excludeEndpoints ) {
            skipSubtree = callback( pointer );
          }
          first = false;
        } else {
          // between point
          skipSubtree = callback( pointer );
        }
        
        if ( skipSubtree && pointer.isBefore ) {
          // to skip the subtree, we just change to isAfter
          pointer.setBefore( false );
          
          // if we skip a subtree, make sure we don't run past the ending pointer
          if ( pointer.compareNested( other ) === 1 ) {
            break;
          }
        } else {
          pointer.nestedForwards();
        }
      }
      
      // end point
      if ( !excludeEndpoints ) {
        callback( pointer );
      }
    },
    
    toString: function() {
      return '[' + ( this.isBefore ? 'before' : 'after' ) + ' ' + this.trail.toString().slice( 1 );
    }
  };
  
  // same as new TrailPointer( trailA, isBeforeA ).compareNested( new TrailPointer( trailB, isBeforeB ) )
  TrailPointer.compareNested = function( trailA, isBeforeA, trailB, isBeforeB ) {
    var comparison = trailA.compare( trailB );
    
    if ( comparison === 0 ) {
      // if trails are equal, just compare before/after
      if ( isBeforeA === isBeforeB ) {
        return 0;
      } else {
        return isBeforeA ? -1 : 1;
      }
    } else {
      // if one is an extension of the other, the shorter isBefore flag determines the order completely
      if ( trailA.isExtensionOf( trailB ) ) {
        return isBeforeB ? 1 : -1;
      } else if ( trailB.isExtensionOf( trailA ) ) {
        return isBeforeA ? -1 : 1;
      } else {
        // neither is a subtrail of the other, so a straight trail comparison should give the answer
        return comparison;
      }
    }
  };
  
  return TrailPointer;
} );


// Copyright 2002-2013, University of Colorado

/**
 * A Canvas-backed layer in the scene graph. Each layer handles dirty-region handling separately,
 * and corresponds to a single canvas / svg element / DOM element in the main container.
 * Importantly, it does not contain rendered content from a subtree of the main
 * scene graph. It only will render a contiguous block of nodes visited in a depth-first
 * manner.
 *
 * Backing store pixel ratio info: http://www.html5rocks.com/en/tutorials/canvas/hidpi/
 *
 * TODO: update internal documentation
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/CanvasLayer',['require','PHET_CORE/inherit','DOT/Bounds2','SCENERY/scenery','KITE/Shape','SCENERY/layers/Layer','SCENERY/util/CanvasContextWrapper','SCENERY/util/Trail','SCENERY/util/TrailPointer','SCENERY/util/Util'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Bounds2 = require( 'DOT/Bounds2' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  var Shape = require( 'KITE/Shape' );
  
  var Layer = require( 'SCENERY/layers/Layer' ); // uses Layer's prototype for inheritance
  require( 'SCENERY/util/CanvasContextWrapper' );
  require( 'SCENERY/util/Trail' );
  require( 'SCENERY/util/TrailPointer' );
  require( 'SCENERY/util/Util' );
  
  // stores CanvasContextWrappers to be re-used
  var canvasContextPool = [];
  
  // assumes main is wrapped with JQuery
  /*
   *
   */
  scenery.CanvasLayer = function CanvasLayer( args ) {
    sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' constructor' );
    Layer.call( this, args );
    
    // TODO: deprecate Scene's backing scale, and handle this on a layer-by-layer option?
    this.backingScale = args.scene.backingScale;
    if ( args.fullResolution !== undefined ) {
      this.backingScale = args.fullResolution ? scenery.Util.backingScale( document.createElement( 'canvas' ).getContext( '2d' ) ) : 1;
    }
    
    this.logicalWidth = this.scene.sceneBounds.width;
    this.logicalHeight = this.scene.sceneBounds.height;
    
    var canvas = document.createElement( 'canvas' );
    canvas.width = this.logicalWidth * this.backingScale;
    canvas.height = this.logicalHeight * this.backingScale;
    canvas.style.width = this.logicalWidth + 'px';
    canvas.style.height = this.logicalHeight + 'px';
    canvas.style.position = 'absolute';
    canvas.style.left = '0';
    canvas.style.top = '0';
    
    // add this layer on top (importantly, the constructors of the layers are called in order)
    this.$main.append( canvas );
    
    this.canvas = canvas;
    // this.context = new DebugContext( canvas.getContext( '2d' ) );
    this.context = canvas.getContext( '2d' );
    this.scene = args.scene;
    
    // workaround for Chrome (WebKit) miterLimit bug: https://bugs.webkit.org/show_bug.cgi?id=108763
    this.context.miterLimit = 20;
    this.context.miterLimit = 10;
    
    this.isCanvasLayer = true;
    
    this.wrapper = new scenery.CanvasContextWrapper( this.canvas, this.context );
    
    this.boundlessCount = 0; // count of how many trails do not support bounds. we only will use dirty region repainting if this number is 0.
  };
  var CanvasLayer = scenery.CanvasLayer;
  
  inherit( Layer, CanvasLayer, {
    
    /*
     * Renders the canvas layer from the scene
     *
     * Supported args: {
     *   fullRender: true, // disables drawing to just dirty rectangles
     *   TODO: pruning with bounds and flag to disable
     * }
     */
    render: function( scene, args ) {
      args = args || {};
      
      var dirtyBoundsEnabled = this.canUseDirtyRegions() && !args.fullRender;
      
      // bail out quickly if possible
      if ( dirtyBoundsEnabled && this.dirtyBounds.isEmpty() ) {
        return;
      }
      
      // switch to an identity transform
      this.context.setTransform( this.backingScale, 0, 0, this.backingScale, 0, 0 );
      
      var visibleDirtyBounds = dirtyBoundsEnabled ? this.dirtyBounds.intersection( scene.sceneBounds ) : scene.sceneBounds;
      
      if ( !visibleDirtyBounds.isEmpty() ) {
        this.clearGlobalBounds( visibleDirtyBounds );
        
        if ( dirtyBoundsEnabled ) {
          this.pushClipShape( Shape.bounds( visibleDirtyBounds ) );
        }
        
        // dirty bounds (clear, possibly set restricted bounds and handling for that)
        // visibility checks
        this.recursiveRender( scene, args );
        
        // exists for now so that we pop the necessary context state
        if ( dirtyBoundsEnabled ) {
          this.popClipShape();
        }
      }
      
      // we rendered everything, no more dirty bounds
      this.dirtyBounds = Bounds2.NOTHING;
    },
    
    recursiveRender: function( scene, args ) {
      var layer = this;
      var i;
      var startPointer = new scenery.TrailPointer( this.startPaintedTrail, true );
      var endPointer = new scenery.TrailPointer( this.endPaintedTrail, true );
      
      // stack for canvases that need to be painted, since some effects require scratch canvases
      var wrapperStack = [ this.wrapper ]; // type {CanvasContextWrapper}
      this.wrapper.resetStyles(); // let's be defensive, save() and restore() may have been called previously
      
      function requiresScratchCanvas( trail ) {
        return trail.lastNode().getOpacity() < 1;
      }
      
      function getCanvasWrapper() {
        var width = layer.logicalWidth * layer.backingScale;
        var height = layer.logicalHeight * layer.backingScale;
        
        if ( canvasContextPool.length ) {
          // use a pooled wrapper
          var wrapper = canvasContextPool.pop();
          wrapper.setDimensions( width, height );
          return wrapper;
        } else {
          // create a new wrapper
          var canvas = document.createElement( 'canvas' );
          canvas.width = layer.logicalWidth * layer.backingScale;
          canvas.height = layer.logicalHeight * layer.backingScale;
          var context = canvas.getContext( '2d' );
          
          return new scenery.CanvasContextWrapper( canvas, context );
        }
      }
      
      function topWrapper() {
        return wrapperStack[wrapperStack.length-1];
      }
      
      function enter( trail ) {
        var node = trail.lastNode();
        
        if ( requiresScratchCanvas( trail ) ) {
          var wrapper = getCanvasWrapper();
          wrapperStack.push( wrapper );
          
          var newContext = wrapper.context;
          
          // switch to an identity transform
          newContext.setTransform( layer.backingScale, 0, 0, layer.backingScale, 0, 0 );
          
          // properly set the necessary transform on the context
          var length = trail.nodes.length;
          for ( var i = 0; i < length; i++ ) {
            trail.nodes[i].transform.getMatrix().canvasAppendTransform( newContext );
          }
        } else {
          node.transform.getMatrix().canvasAppendTransform( topWrapper().context );
        }
        
        if ( node._clipShape ) {
          // TODO: move to wrapper-specific part
          layer.pushClipShape( node._clipShape );
        }
      }
      
      function exit( trail ) {
        var node = trail.lastNode();
        
        if ( node._clipShape ) {
          // TODO: move to wrapper-specific part
          layer.popClipShape();
        }
        
        if ( requiresScratchCanvas( trail ) ) {
          var baseContext = wrapperStack[wrapperStack.length-2].context;
          var topCanvas = wrapperStack[wrapperStack.length-1].canvas;
          
          // apply necessary style transforms before painting our popped canvas onto the next canvas
          var opacityChange = trail.lastNode().getOpacity() < 1;
          if ( opacityChange ) {
            baseContext.globalAlpha = trail.lastNode().getOpacity();
          }
          
          // paint our canvas onto the level below with a straight transform
          baseContext.save();
          baseContext.setTransform( 1, 0, 0, 1, 0, 0 );
          baseContext.drawImage( topCanvas, 0, 0 );
          baseContext.restore();
          
          // reset styles
          if ( opacityChange ) {
            baseContext.globalAlpha = 1;
          }
          
          var wrapper = wrapperStack.pop();
          if ( wrapper !== layer.wrapper ) {
            // store the CanvasContextWrapper for recycling if it isn't our core wrapper
            canvasContextPool.push( wrapper );
          }
        } else {
          node.transform.getInverse().canvasAppendTransform( topWrapper().context );
        }
      }
      
      /*
       * We count how many invisible nodes are in our trail, so we can properly iterate without inspecting everything.
       * Additionally, state changes (enter/exit) are only done when nodes are visible, so we skip overhead. If
       * invisibleCount > 0, then the current node is invisible.
       */
      var invisibleCount = 0;
      
      var boundaryTrail;
      
      // sanity check, and allows us to get faster speed
      startPointer.trail.reindex();
      endPointer.trail.reindex();
      
      // first, we need to walk the state up to before our pointer (as far as the recursive handling is concerned)
      // if the pointer is 'before' the node, don't call its enterState since this will be taken care of as the first step.
      // if the pointer is 'after' the node, call enterState since it will call exitState immediately inside the loop
      var startWalkLength = startPointer.trail.length - ( startPointer.isBefore ? 1 : 0 );
      boundaryTrail = new scenery.Trail();
      for ( i = 0; i < startWalkLength; i++ ) {
        var startNode = startPointer.trail.nodes[i];
        boundaryTrail.addDescendant( startNode );
        invisibleCount += startNode.isVisible() ? 0 : 1;
        
        if ( invisibleCount === 0 ) {
          // walk up initial state
          enter( boundaryTrail );
        }
      }
      
      startPointer.depthFirstUntil( endPointer, function renderPointer( pointer ) {
        // handle render here
        
        var node = pointer.trail.lastNode();
        
        if ( pointer.isBefore ) {
          invisibleCount += node.isVisible() ? 0 : 1;
          
          if ( invisibleCount === 0 ) {
            enter( pointer.trail );
            
            if ( node.isPainted() ) {
              var wrapper = wrapperStack[wrapperStack.length-1];
              
              // TODO: consider just passing the wrapper. state not needed (for now), context easily accessible
              node.paintCanvas( wrapper );
            }
            
            // TODO: restricted bounds rendering, and possibly generalize depthFirstUntil
            // var children = node._children;
            
            // check if we need to filter the children we render, and ignore nodes with few children (but allow 2, since that may prevent branches)
            // if ( state.childRestrictedBounds && children.length > 1 ) {
            //   var localRestrictedBounds = node.globalToLocalBounds( state.childRestrictedBounds );
              
            //   // don't filter if every child is inside the bounds
            //   if ( !localRestrictedBounds.containsBounds( node.parentToLocalBounds( node._bounds ) ) ) {
            //     children = node.getChildrenWithinBounds( localRestrictedBounds );
            //   }
            // }
            
            // _.each( children, function( child ) {
            //   fullRender( child, state );
            // } );
          } else {
            // not visible, so don't render the entire subtree
            return true;
          }
        } else {
          if ( invisibleCount === 0 ) {
            exit( pointer.trail );
          }
          
          invisibleCount -= node.isVisible() ? 0 : 1;
        }
        
      }, false ); // include endpoints (for now)
      
      // then walk the state back so we don't muck up any context saving that is going on, similar to how we walked it at the start
      // if the pointer is 'before' the node, call exitState since it called enterState inside the loop on it
      // if the pointer is 'after' the node, don't call its exitState since this was already done
      boundaryTrail = endPointer.trail.copy();
      var endWalkLength = endPointer.trail.length - ( endPointer.isAfter ? 1 : 0 );
      for ( i = endWalkLength - 1; i >= 0; i-- ) {
        var endNode = endPointer.trail.nodes[i];
        invisibleCount -= endNode.isVisible() ? 0 : 1;
        
        if ( invisibleCount === 0 ) {
          // walk back the state
          exit( boundaryTrail );
        }
        
        boundaryTrail.removeDescendant();
      }
    },
    
    dispose: function() {
      Layer.prototype.dispose.call( this );
      
      this.canvas.parentNode.removeChild( this.canvas );
    },
    
    // TODO: consider a stack-based model for transforms?
    applyTransformationMatrix: function( matrix ) {
      matrix.canvasAppendTransform( this.context );
    },
    
    // returns next zIndex in place. allows layers to take up more than one single zIndex
    reindex: function( zIndex ) {
      Layer.prototype.reindex.call( this, zIndex );
      
      if ( this.zIndex !== zIndex ) {
        this.canvas.style.zIndex = zIndex;
        this.zIndex = zIndex;
      }
      return zIndex + 1;
    },
    
    pushClipShape: function( shape ) {
      // store the current state, since browser support for context.resetClip() is not yet in the stable browser versions
      this.context.save();
      
      this.writeClipShape( shape );
    },
    
    popClipShape: function() {
      this.context.restore();
    },
    
    // canvas-specific
    writeClipShape: function( shape ) {
      // set up the clipping
      this.context.beginPath();
      shape.writeToContext( this.context );
      this.context.clip();
    },
    
    clearGlobalBounds: function( bounds ) {
      if ( !bounds.isEmpty() ) {
        this.context.save();
        this.context.setTransform( this.backingScale, 0, 0, this.backingScale, 0, 0 );
        this.context.clearRect( bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() );
        // use this for debugging cleared (dirty) regions for now
        // this.context.fillStyle = '#' + Math.floor( Math.random() * 0xffffff ).toString( 16 );
        // this.context.fillRect( bounds.x, bounds.y, bounds.width, bounds.height );
        this.context.restore();
      }
    },
    
    getSVGString: function() {
      return '<image xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="' + this.canvas.toDataURL() + '" x="0" y="0" height="' + this.canvas.height + 'px" width="' + this.canvas.width + 'px"/>';
    },
    
    // TODO: note for DOM we can do https://developer.mozilla.org/en-US/docs/HTML/Canvas/Drawing_DOM_objects_into_a_canvas
    renderToCanvas: function( canvas, context, delayCounts ) {
      context.drawImage( this.canvas, 0, 0 );
    },
    
    addInstance: function( instance ) {
      var trail = instance.trail;
      
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' addInstance: ' + trail.toString() );
      Layer.prototype.addInstance.call( this, instance );
      
      // since the node's getBounds() are in the parent coordinate frame, we peel off the last node to get the correct (relevant) transform
      // TODO: more efficient way of getting this transform?
      this.canvasMarkLocalBounds( trail.lastNode().getBounds(), trail.slice( 0, trail.length - 1 ) );
      
      if ( trail.lastNode().boundsInaccurate ) {
        this.boundlessCount++;
      }
    },
    
    removeInstance: function( instance ) {
      var trail = instance.trail;
      
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' removeInstance: ' + trail.toString() );
      Layer.prototype.removeInstance.call( this, instance );
      
      // since the node's getBounds() are in the parent coordinate frame, we peel off the last node to get the correct (relevant) transform
      // TODO: more efficient way of getting this transform?
      this.canvasMarkLocalBounds( trail.lastNode().getBounds(), trail.slice( 0, trail.length - 1 ) );
      
      if ( trail.lastNode().boundsInaccurate ) {
        this.boundlessCount--;
      }
    },
    
    canUseDirtyRegions: function() {
      sceneryAssert && sceneryAssert( this.boundlessCount >= 0 );
      return this.boundlessCount === 0;
    },
    
    // NOTE: for performance, we will mutate the bounds passed in (they are almost assuredly from the local or parent bounds functions)
    canvasMarkGlobalBounds: function( globalBounds ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' canvasMarkGlobalBounds: ' + globalBounds.toString() );
      sceneryAssert && sceneryAssert( globalBounds.isEmpty() || globalBounds.isFinite(), 'Infinite (non-empty) dirty bounds passed to canvasMarkGlobalBounds' );
      
      // TODO: for performance, consider more than just a single dirty bounding box
      this.dirtyBounds = this.dirtyBounds.union( globalBounds.dilate( 2 ).roundOut() );
    },
    
    canvasMarkLocalBounds: function( localBounds, trail ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' canvasMarkLocalBounds: ' + localBounds.toString() + ' on ' + trail.toString() );
      if ( !this.canUseDirtyRegions() ) {
        this.dirtyBounds = Bounds2.EVERYTHING;
      } else {
        this.canvasMarkGlobalBounds( trail.localToGlobalBounds( localBounds ) );
      }
    },
    
    canvasMarkParentBounds: function( parentBounds, trail ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' canvasMarkParentBounds: ' + parentBounds.toString() + ' on ' + trail.toString() );
      if ( !this.canUseDirtyRegions() ) {
        this.dirtyBounds = Bounds2.EVERYTHING;
      } else {
        this.canvasMarkGlobalBounds( trail.parentToGlobalBounds( parentBounds ) );
      }
    },
    
    canvasMarkSelf: function( instance ) {
      this.canvasMarkLocalBounds( instance.getNode().getSelfBounds(), instance.trail );
    },
    
    canvasMarkSubtree: function( instance ) {
      this.canvasMarkParentBounds( instance.getNode().getBounds(), instance.trail );
    },
    
    getName: function() {
      return 'canvas';
    },
    
    /*---------------------------------------------------------------------------*
    * Events from Instances
    *----------------------------------------------------------------------------*/
    
    notifyVisibilityChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' notifyVisibilityChange: ' + instance.trail.toString() );
      // old paint taken care of in notifyBeforeSubtreeChange()
      
      if ( instance.trail.isVisible() ) {
        this.canvasMarkSubtree( instance );
      }
    },
    
    notifyOpacityChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' notifyOpacityChange: ' + instance.trail.toString() );
      // old paint taken care of in notifyBeforeSubtreeChange()
      
      this.canvasMarkSubtree( instance );
    },
    
    // only a painted trail under this layer
    notifyBeforeSelfChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' notifyBeforeSelfChange: ' + instance.trail.toString() );
      this.canvasMarkSelf( instance );
    },
    
    notifyBeforeSubtreeChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' notifyBeforeSubtreeChange: ' + instance.trail.toString() );
      this.canvasMarkSubtree( instance );
    },
    
    // only a painted trail under this layer
    notifyDirtySelfPaint: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' notifyDirtySelfPaint: ' + instance.trail.toString() );
      this.canvasMarkSelf( instance );
    },
    
    notifyDirtySubtreePaint: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' notifyDirtySubtreePaint: ' + instance.trail.toString() );
      this.canvasMarkSubtree( instance );
    },
    
    notifyTransformChange: function( instance ) {
      // sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' notifyTransformChange: ' + instance.trail.toString() );
      // TODO: how best to mark this so if there are multiple 'movements' we don't get called more than needed?
      // this.canvasMarkSubtree( instance );
    },
    
    // only a painted trail under this layer (for now)
    notifyBoundsAccuracyChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'CanvasLayer #' + this.id + ' notifyBoundsAccuracyChange: ' + instance.trail.toString() );
      
      if ( instance.node.boundsInaccurate ) {
        this.boundlessCount++;
      } else {
        this.boundlessCount--;
      }
    }
  } );
  
  return CanvasLayer;
} );



// Copyright 2002-2013, University of Colorado

/**
 * A DOM-based layer in the scene graph. Each layer handles dirty-region handling separately,
 * and corresponds to a single canvas / svg element / DOM element in the main container.
 * Importantly, it does not contain rendered content from a subtree of the main
 * scene graph. It only will render a contiguous block of nodes visited in a depth-first
 * manner.
 *
 * Nodes supporting the DOM renderer should have the following functions:
 *   allowsMultipleDOMInstances:               {Boolean} whether getDOMElement will return the same element every time, or new elements.
 *   getDOMElement():                          Returns a DOM element that represents this node.
 *   updateDOMElement( element ):              Updates the DOM element with any changes that were made.
 *   updateCSSTransform( transform, element ): Updates the CSS transform of the element
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/DOMLayer',['require','PHET_CORE/inherit','DOT/Bounds2','SCENERY/scenery','SCENERY/layers/Layer','SCENERY/util/Trail'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Bounds2 = require( 'DOT/Bounds2' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  var Layer = require( 'SCENERY/layers/Layer' ); // DOMLayer inherits from Layer
  require( 'SCENERY/util/Trail' );
  
  scenery.DOMLayer = function DOMLayer( args ) {
    sceneryLayerLog && sceneryLayerLog( 'DOMLayer constructor' );
    
    Layer.call( this, args );
    
    var width = args.scene.sceneBounds.width;
    var height = args.scene.sceneBounds.height;
    
    this.div = document.createElement( 'div' );
    var div = this.div;
    div.style.position = 'absolute';
    div.style.left = '0';
    div.style.top = '0';
    div.style.width = '0';
    div.style.height = '0';
    div.style.clip = 'rect(0px,' + width + 'px,' + height + 'px,0px)';
    this.$div = $( this.div );
    this.$main.append( this.div );
    
    this.scene = args.scene; // TODO: should already be set in the supertype Layer
    
    this.isDOMLayer = true;
    
    // maps trail ID => DOM element fragment
    this.idElementMap = {};
    
    // maps trail ID => Trail. trails need to be reindexed
    this.idTrailMap = {};
    
    this.trails = [];
  };
  var DOMLayer = scenery.DOMLayer;
  
  inherit( Layer, DOMLayer, {
    
    addInstance: function( instance ) {
      Layer.prototype.addInstance.call( this, instance );
      
      var trail = instance.trail;
      this.reindexTrails();
      
      var node = trail.lastNode();
      
      var element = node.getDOMElement();
      node.updateDOMElement( element );
      this.updateVisibility( trail, element );
      
      this.idElementMap[trail.getUniqueId()] = element;
      this.idTrailMap[trail.getUniqueId()] = trail;
      
      // walk the insertion index up the array. TODO: performance: binary search version?
      var insertionIndex;
      for ( insertionIndex = 0; insertionIndex < this.trails.length; insertionIndex++ ) {
        var otherTrail = this.trails[insertionIndex];
        otherTrail.reindex();
        var comparison = otherTrail.compare( trail );
        sceneryAssert && sceneryAssert( comparison !== 0, 'Trail has already been inserted into the DOMLayer' );
        if ( comparison === 1 ) { // TODO: enum values!
          break;
        }
      }
      
      if ( insertionIndex === this.div.childNodes.length ) {
        this.div.appendChild( element );
        this.trails.push( trail );
      } else {
        this.div.insertBefore( this.getElementFromTrail( this.trails[insertionIndex] ) );
        this.trails.splice( insertionIndex, 0, trail );
      }
      node.updateCSSTransform( trail.getTransform(), element );
    },
    
    removeInstance: function( instance ) {
      Layer.prototype.removeInstance.call( this, instance );
      
      var trail = instance.trail;
      this.reindexTrails();
      
      var element = this.getElementFromTrail( trail );
      sceneryAssert && sceneryAssert( element, 'Trail does not exist in the DOMLayer' );
      
      delete this.idElementMap[trail.getUniqueId];
      delete this.idTrailMap[trail.getUniqueId];
      
      this.div.removeChild( element );
      
      var removalIndex = this.getIndexOfTrail( trail );
      this.trails.splice( removalIndex, 1 );
    },
    
    getElementFromTrail: function( trail ) {
      return this.idElementMap[trail.getUniqueId()];
    },
    
    reindexTrails: function( zIndex ) {
      Layer.prototype.reindex.call( this, zIndex );
      
      var i = this.trails.length;
      while ( i-- ) {
        this.trails[i].reindex();
      }
    },
    
    getIndexOfTrail: function( trail ) {
      // find the index where our trail is at. strict equality won't work, we want to compare differently
      var i;
      for ( i = 0; i < this.trails.length; i++ ) {
        if ( this.trails[i].compare( trail ) === 0 ) {
          return i;
        }
      }
      throw new Error( 'DOMLayer.getIndexOfTrail unable to find trail: ' + trail.toString() );
    },
    
    render: function( scene, args ) {
      // nothing at all needed here, CSS transforms taken care of when dirty regions are notified
    },
    
    dispose: function() {
      Layer.prototype.dispose.call( this );
      
      this.div.parentNode.removeChild( this.div );
    },
    
    updateVisibility: function( trail, element ) {
      if ( trail.isVisible() ) {
        element.style.visibility = 'visible';
      } else {
        element.style.visibility = 'hidden';
      }
    },
    
    // TODO: consider a stack-based model for transforms?
    // TODO: deprecated? remove this?
    applyTransformationMatrix: function( matrix ) {
      // nothing at all needed here
    },
    
    getContainer: function() {
      return this.div;
    },
    
    // returns next zIndex in place. allows layers to take up more than one single zIndex
    reindex: function( zIndex ) {
      Layer.prototype.reindex.call( this, zIndex );
      
      if ( this.zIndex !== zIndex ) {
        this.div.style.zIndex = zIndex;
        this.zIndex = zIndex;
      }
      return zIndex + 1;
    },
    
    pushClipShape: function( shape ) {
      // TODO: clipping
    },
    
    popClipShape: function() {
      // TODO: clipping
    },
    
    getSVGString: function() {
      var data = "<svg xmlns='http://www.w3.org/2000/svg' width='" + this.$main.width() + "' height='" + this.$main.height() + "'>" +
        "<foreignObject width='100%' height='100%'>" +
        $( this.div ).html() +
        "</foreignObject></svg>";
    },
    
    // TODO: note for DOM we can do https://developer.mozilla.org/en-US/docs/HTML/Canvas/Drawing_DOM_objects_into_a_canvas
    // TODO: note that http://pbakaus.github.com/domvas/ may work better, but lacks IE support
    renderToCanvas: function( canvas, context, delayCounts ) {
      // TODO: consider not silently failing?
      // var data = "<svg xmlns='http://www.w3.org/2000/svg' width='" + this.$main.width() + "' height='" + this.$main.height() + "'>" +
      //   "<foreignObject width='100%' height='100%'>" +
      //   $( this.div ).html() +
      //   "</foreignObject></svg>";
      
      // var DOMURL = window.URL || window.webkitURL || window;
      // var img = new Image();
      // var svg = new Blob( [ data ] , { type: "image/svg+xml;charset=utf-8" } );
      // var url = DOMURL.createObjectURL( svg );
      // delayCounts.increment();
      // img.onload = function() {
      //   context.drawImage( img, 0, 0 );
      //   // TODO: this loading is delayed!!! ... figure out a solution to potentially delay?
      //   DOMURL.revokeObjectURL( url );
      //   delayCounts.decrement();
      // };
      // img.src = url;
    },
    
    getName: function() {
      return 'dom';
    },
    
    /*---------------------------------------------------------------------------*
    * Events from Instances
    *----------------------------------------------------------------------------*/
    
    notifyVisibilityChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'DOMLayer #' + this.id + ' notifyVisibilityChange: ' + instance.trail.toString() );
      var trail = instance.trail;
      
      // TODO: performance: faster way to iterate through!
      for ( var trailId in this.idTrailMap ) {
        var subtrail = this.idTrailMap[trailId];
        subtrail.reindex();
        if ( subtrail.isExtensionOf( trail, true ) ) {
          this.updateVisibility( subtrail, this.idElementMap[trailId] );
        }
      }
    },
    
    notifyOpacityChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'DOMLayer #' + this.id + ' notifyOpacityChange: ' + instance.trail.toString() );
      // TODO: BROKEN: FIXME: DOM opacity is not handled yet, see issue #31: https://github.com/phetsims/scenery/issues/31
    },
    
    // only a painted trail under this layer
    notifyBeforeSelfChange: function( instance ) {
      // sceneryLayerLog && sceneryLayerLog( 'DOMLayer #' + this.id + ' notifyBeforeSelfChange: ' + instance.trail.toString() );
      // no-op, we don't need paint changes
    },
    
    notifyBeforeSubtreeChange: function( instance ) {
      // sceneryLayerLog && sceneryLayerLog( 'DOMLayer #' + this.id + ' notifyBeforeSubtreeChange: ' + instance.trail.toString() );
      // no-op, we don't need paint changes
    },
    
    // only a painted trail under this layer
    notifyDirtySelfPaint: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'DOMLayer #' + this.id + ' notifyDirtySelfPaint: ' + instance.trail.toString() );
      var node = instance.getNode();
      var trail = instance.trail;
      
      // TODO: performance: store this in the Instance itself?
      var dirtyElement = this.idElementMap[trail.getUniqueId()];
      if ( dirtyElement ) {
        node.updateDOMElement( dirtyElement );
        
        if ( node.domUpdateTransformOnRepaint ) {
          node.updateCSSTransform( trail.getTransform(), dirtyElement );
        }
      }
    },

    notifyDirtySubtreePaint: function( instance ) {
      if ( instance.layer === this ) {
        this.notifyDirtySelfPaint( instance );
      }
    },
    
    notifyTransformChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'DOMLayer #' + this.id + ' notifyTransformChange: ' + instance.trail.toString() );
      var layer = this;
      
      var baseTrail = instance.trail;
      
      // TODO: performance: efficiency! this computes way more matrix transforms than needed
      scenery.Trail.eachPaintedTrailBetween( this.startPaintedTrail, this.endPaintedTrail, function( trail ) {
        if ( trail.isExtensionOf( baseTrail, true ) ) {
          // TODO: put the element on the instance?
          var element = layer.idElementMap[trail.getUniqueId()];
          var node = trail.lastNode();
          node.updateCSSTransform( trail.getTransform(), element );
        }
      }, false, this.scene );
    },
    
    // only a painted trail under this layer (for now)
    notifyBoundsAccuracyChange: function( instance ) {
      // sceneryLayerLog && sceneryLayerLog( 'DOMLayer #' + this.id + ' notifyBoundsAccuracyChange: ' + instance.trail.toString() );
      // no-op, we don't care about bounds
    }
    
  } );
  
  return DOMLayer;
} );



// Copyright 2002-2013, University of Colorado

/**
 * An SVG-based layer in the scene graph. Each layer handles dirty-region handling separately,
 * and corresponds to a single canvas / svg element / DOM element in the main container.
 * Importantly, it does not contain rendered content from a subtree of the main
 * scene graph. It only will render a contiguous block of nodes visited in a depth-first
 * manner.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/SVGLayer',['require','PHET_CORE/inherit','DOT/Bounds2','DOT/Transform3','DOT/Matrix3','SCENERY/scenery','SCENERY/layers/Layer','SCENERY/util/Trail','SCENERY/util/Util'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Transform3 = require( 'DOT/Transform3' );
  var Matrix3 = require( 'DOT/Matrix3' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  var Layer = require( 'SCENERY/layers/Layer' ); // extends Layer
  require( 'SCENERY/util/Trail' );
  require( 'SCENERY/util/Util' );
  
  // used namespaces
  var svgns = 'http://www.w3.org/2000/svg';
  var xlinkns = 'http://www.w3.org/1999/xlink';
  
  scenery.SVGLayer = function SVGLayer( args ) {
    sceneryLayerLog && sceneryLayerLog( 'SVGLayer constructor' );
    var $main = args.$main;
    
    this.scene = args.scene;
    
    // main SVG element
    this.svg = document.createElementNS( svgns, 'svg' );
    
    // the SVG has a single group under it, which corresponds to the transform of the layer's base node
    // TODO: consider renaming to 'this.baseGroup'
    this.g = document.createElementNS( svgns, 'g' );
    
    // the <defs> block that we will be stuffing gradients and patterns into
    this.defs = document.createElementNS( svgns, 'defs' );
    
    var width = args.scene.sceneBounds.width;
    var height = args.scene.sceneBounds.height;
    
    this.svg.appendChild( this.defs );
    this.svg.appendChild( this.g );
    this.$svg = $( this.svg );
    this.svg.setAttribute( 'width', width );
    this.svg.setAttribute( 'height', height );
    this.svg.setAttribute( 'stroke-miterlimit', 10 ); // to match our Canvas brethren so we have the same default behavior
    this.svg.style.position = 'absolute';
    this.svg.style.left = '0';
    this.svg.style.top = '0';
    this.svg.style.clip = 'rect(0px,' + width + 'px,' + height + 'px,0px)';
    this.svg.style['pointer-events'] = 'none';
    $main.append( this.svg );
    
    this.isSVGLayer = true;
    
    // maps trail ID => SVG self fragment (that displays shapes, text, etc.)
    this.idFragmentMap = {};
    
    // maps trail ID => SVG <g> that contains that node's self and everything under it
    this.idGroupMap = {};
    
    
    Layer.call( this, args );
    
    this.baseTransformDirty = true;
    this.baseTransformChange = true;
  };
  var SVGLayer = scenery.SVGLayer;
  
  // used as an object pool for marking internal base node bounds
  
  var scratchBounds1 = Bounds2.NOTHING.copy();
  
  inherit( Layer, SVGLayer, {
    
    /*
     * Notes about how state is tracked here:
     * Trails are stored on group.trail so that we can look this up when inserting new groups
     */
    addInstance: function( instance ) {
      var trail = instance.trail;
      
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' addInstance: ' + trail.toString() );
      
      sceneryAssert && sceneryAssert( !( trail.getUniqueId() in this.idFragmentMap ), 'Already contained that trail!' );
      sceneryAssert && sceneryAssert( trail.isPainted(), 'Don\'t add nodes without isPainted() to SVGLayer' );
      
      Layer.prototype.addInstance.call( this, instance );
      
      var subtrail = this.baseTrail.copy(); // grab the trail up to (and including) the base node, so we don't create superfluous groups
      var lastId = null;
      
      // walk a subtrail up from the root node all the way to the full trail, creating groups where necessary
      while ( subtrail.length <= trail.length ) {
        var id = subtrail.getUniqueId();
        var group = this.idGroupMap[id];
        
        if ( !group ) {
          // we need to create a new group
          
          if ( lastId ) {
            // we have a parent group to which we need to be added
            group = document.createElementNS( svgns, 'g' );
            
            // apply the node's transform to the group
            this.applyTransform( subtrail.lastNode().getTransform(), group );
            
            // add the group to its parent
            this.insertGroupIntoParent( group, this.idGroupMap[lastId], subtrail );
          } else {
            // we are ensuring the base group
            sceneryAssert && sceneryAssert( subtrail.lastNode() === this.baseNode );
            
            group = this.g;
            
            // sets up the proper transform for the base
            this.initializeBase();
            
            // immediately update the base transform so we don't temporarily display.
            // fixes https://github.com/phetsims/beers-law-lab/issues/20
            this.refreshBaseTransform();
          }
          
          // apply any stylings to the group (opacity, visibility)
          this.updateNodeGroup( subtrail.lastNode(), group );
          
          group.referenceCount = 0; // initialize a reference count, so we can know when to remove unused groups
          group.trail = subtrail.copy(); // put a reference to the trail on the group, so we can efficiently scan and see where to insert future groups
          
          this.idGroupMap[id] = group;
        }
        
        // this trail will depend on this group, so increment the reference counter
        group.referenceCount++;
        
        if ( subtrail.length === trail.length ) {
          // TODO: cleaner control structures
          break;
        }
        
        // step down towards our full trail
        subtrail.addDescendant( trail.nodes[subtrail.length] );
        lastId = id;
      }
      
      // actually add the node into its own group
      var node = trail.lastNode();
      var trailId = trail.getUniqueId();
      
      var nodeGroup = this.idGroupMap[trailId];
      var svgFragment = node.createSVGFragment( this.svg, this.defs, nodeGroup );
      this.updateNode( node, svgFragment );
      this.updateNodeGroup( node, nodeGroup );
      this.idFragmentMap[trailId] = svgFragment;
      nodeGroup.appendChild( svgFragment );
    },
    
    removeInstance: function( instance ) {
      var trail = instance.trail;
      
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' removeInstance: ' + trail.toString() );
      sceneryAssert && sceneryAssert( trail.getUniqueId() in this.idFragmentMap, 'Did not contain that trail!' );
      
      Layer.prototype.removeInstance.call( this, instance );
      
      // clean up the fragment and defs directly died to the node
      var trailId = trail.getUniqueId();
      var node = trail.lastNode();
      var fragment = this.idFragmentMap[trailId];
      this.idGroupMap[trailId].removeChild( fragment );
      delete this.idFragmentMap[trailId];
      if ( node.removeSVGDefs ) {
        node.removeSVGDefs( this.svg, this.defs );
      }
      
      // clean up any unneeded groups
      var subtrail = trail.copy();
      while ( subtrail.length > this.baseTrail.length ) {
        var id = subtrail.getUniqueId();
        
        var group = this.idGroupMap[id];
        group.referenceCount--;
        if ( group.referenceCount === 0 ) {
          // completely kill the group
          group.parentNode.removeChild( group );
          delete group.trail; // just in case someone held a reference
          delete this.idGroupMap[id];
        }
        
        subtrail.removeDescendant();
      }
      this.g.referenceCount--; // since we don't go down to the base group, adjust its reference count
    },
    
    // subtrail is to group, and should include parentGroup below
    insertGroupIntoParent: function( group, parentGroup, subtrail ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' insertGroupIntoParent subtrail:' + subtrail.toString() );
      if ( !parentGroup.childNodes.length ) {
        parentGroup.appendChild( group );
      } else {
        // if there is already a child, we need to do a scan to ensure we place our group as a child in the correct order (above/below)
        
        // scan other child groups in the parentGroup to find where we need to be (index i)
        var afterNode = null;
        var indexIndex = subtrail.length - 2; // index into the trail's indices
        var ourIndex = subtrail.indices[indexIndex];
        var i;
        for ( i = 0; i < parentGroup.childNodes.length; i++ ) {
          var child = parentGroup.childNodes[i];
          if ( child.trail ) {
            child.trail.reindex();
            var otherIndex = child.trail.indices[indexIndex];
            if ( otherIndex > ourIndex ) {
              // this other group is above us
              break;
            }
          }
        }
        
        // insert our group before parentGroup.childNodes[i] (or append if that doesn't exist)
        if ( i === parentGroup.childNodes.length ) {
          parentGroup.appendChild( group );
        } else {
          parentGroup.insertBefore( group, parentGroup.childNodes[i] );
        }
      }
    },
    
    // updates visual styles on an existing SVG fragment
    updateNode: function( node, fragment ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' updateNode: ' + node.constructor.name + ' #' + node.id );
      if ( node.updateSVGFragment ) {
        node.updateSVGFragment( fragment );
      }
      if ( node.updateSVGDefs ) {
        node.updateSVGDefs( this.svg, this.defs );
      }
    },
    
    // updates necessary paint attributes on a group (not including transform)
    updateNodeGroup: function( node, group ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' updateNodeGroup: ' + node.constructor.name + ' #' + node.id );
      this.updateGroupVisibility( node, group );
      this.updateGroupOpacity( node, group );
    },
    
    updateGroupVisibility: function( node, group ) {
      // if we're updating visibility for the base trail, apply its visibility and everything beneath it
      if ( node === this.baseNode ? this.baseTrail.isVisible() : node.isVisible() ) {
        group.style.display = 'inherit';
      } else {
        group.style.display = 'none';
      }
    },
    
    updateGroupOpacity: function( node, group ) {
      var opacity;
      if ( node === this.baseNode ) {
        opacity = this.baseTrail.getOpacity(); // multiplied by opacities of all ancestors
      } else {
        opacity = node.getOpacity();
      }
      group.setAttribute( 'opacity', opacity );
    },
    
    getFragmentFromInstance: function( instance ) {
      // TODO: performance: key optimization point for SVG layers
      return this.idFragmentMap[instance.trail.getUniqueId()];
    },
    
    getGroupFromInstance: function( instance ) {
      // TODO: performance: key optimization point for SVG layers
      return this.idGroupMap[instance.trail.getUniqueId()];
    },
    
    applyTransform: function( transform, group ) {
      if ( transform.isIdentity() ) {
        if ( group.hasAttribute( 'transform' ) ) {
          group.removeAttribute( 'transform' );
        }
      } else {
        group.setAttribute( 'transform', transform.getMatrix().getSVGTransform() );
      }
    },
    
    render: function( scene, args ) {
      this.refreshBaseTransform();
    },
    
    refreshBaseTransform: function() {
      if ( this.baseTransformDirty ) {
        // this will be run either now or at the end of flushing changes
        var includesBaseTransformChange = this.baseTransformChange;
        this.updateBaseTransform( includesBaseTransformChange );
        
        this.baseTransformDirty = false;
        this.baseTransformChange = false;
      }
    },
    
    dispose: function() {
      Layer.prototype.dispose.call( this );
      
      this.svg.parentNode.removeChild( this.svg );
    },
    
    markBaseTransformDirty: function( changed ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' markBaseTransformDirty' );
      var baseTransformChange = this.baseTransformChange || !!changed;
      this.baseTransformDirty = true;
      this.baseTransformChange = baseTransformChange;
    },
    
    initializeBase: function() {
      // we don't want to call updateBaseTransform() twice, since baseNodeInternalBoundsChange() will call it if we use CSS transform
      if ( this.cssTransform ) {
        this.baseNodeInternalBoundsChange();
      } else {
        this.markBaseTransformDirty( true );
      }
    },
    
    // called when the base node's "internal" (self or child) bounds change, but not when it is just from the base node's own transform changing
    baseNodeInternalBoundsChange: function() {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' baseNodeInternalBoundsChange' );
      if ( this.cssTransform ) {
        // we want to set the baseNodeTransform to a translation so that it maps the baseNode's self/children in the baseNode's local bounds to (0,0,w,h)
        var internalBounds = scratchBounds1; // pooled copy
        internalBounds.setBounds( this.baseNode.getBounds() );
        this.baseNode.transformBoundsFromParentToLocal( internalBounds );
        var padding = scenery.Layer.cssTransformPadding;
        
        // if there is nothing, or the bounds are empty for some reason, skip this!
        if ( !internalBounds.isEmpty() ) {
          this.baseNodeTransform.set( Matrix3.translation( Math.ceil( -internalBounds.minX + padding), Math.ceil( -internalBounds.minY + padding ) ) );
          
          // NOTE: this is mutable! don't use internalBounds after this
          var baseNodeInteralBounds = internalBounds.transform( this.baseNodeTransform.getMatrix() );
          
          // sanity check to ensure we are within that range
          sceneryAssert && sceneryAssert( baseNodeInteralBounds.minX >= 0 && baseNodeInteralBounds.minY >= 0 );
          
          this.updateContainerDimensions( Math.ceil( baseNodeInteralBounds.maxX + padding ),
                                          Math.ceil( baseNodeInteralBounds.maxY + padding ) );
        }
        
        // if this gets removed, update initializeBase()
        this.markBaseTransformDirty( true );
      } else if ( this.usesPartialCSSTransforms ) {
        this.markBaseTransformDirty( true );
      }
    },
    
    updateContainerDimensions: function( width, height ) {
      this.svg.setAttribute( 'width', width );
      this.svg.setAttribute( 'height', height );
    },
    
    updateBaseTransform: function( includesBaseTransformChange ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' updateBaseTransform' );
      var transform = this.baseTrail.getTransform(); // TODO: consider improving this, CSS+SVG bottleneck
      
      if ( this.cssTransform ) {
        // set the full transform!
        scenery.Util.applyCSSTransform( transform.getMatrix().timesMatrix( this.baseNodeTransform.getInverse() ), this.svg );
        
        if ( includesBaseTransformChange ) {
          this.applyTransform( this.baseNodeTransform, this.g );
        }
      } else if ( this.usesPartialCSSTransforms ) {
        // calculate what our CSS transform should be
        var cssTransform = new Transform3();
        var matrix = transform.getMatrix();
        if ( this.cssTranslation ) {
          cssTransform.append( Matrix3.translation( matrix.m02(), matrix.m12() ) );
        }
        if ( this.cssRotation ) {
          cssTransform.append( Matrix3.rotation2( matrix.getRotation() ) );
        }
        if ( this.cssScale ) {
          var scaleVector = matrix.getScaleVector();
          cssTransform.append( Matrix3.scaling( scaleVector.x, scaleVector.y ) );
        }
        
        // take the CSS transform out of what we will apply to the group
        transform.prepend( cssTransform.getInverse() );
        
        // now we need to see where our baseNode bounds are mapped to with our transform,
        // so that we can apply an extra translation and adjust dimensions as necessary
        var padding = scenery.Layer.cssTransformPadding;
        var internalBounds = this.baseNode.parentToLocalBounds( this.baseNode.getBounds() );
        var mappedBounds = transform.transformBounds2( internalBounds );
        var translation = Matrix3.translation( Math.ceil( -mappedBounds.minX + padding ), Math.ceil( -mappedBounds.minY + padding ) );
        var inverseTranslation = translation.inverted();
        this.updateContainerDimensions( Math.ceil( mappedBounds.getWidth()  + 2 * padding ),
                                        Math.ceil( mappedBounds.getHeight() + 2 * padding ) );
        
        // put the translation adjustment and its inverse in-between the two transforms
        cssTransform.append( inverseTranslation );
        transform.prepend( translation );
        
        // apply the transforms
        // TODO: checks to make sure we don't apply them in a row if one didn't change!
        scenery.Util.applyCSSTransform( cssTransform.getMatrix(), this.svg );
        this.applyTransform( transform, this.g );
      } else {
        this.applyTransform( transform, this.g );
      }
    },
    
    // TODO: consider a stack-based model for transforms?
    applyTransformationMatrix: function( matrix ) {
      // nothing at all needed here
    },
    
    getContainer: function() {
      return this.svg;
    },
    
    // returns next zIndex in place. allows layers to take up more than one single zIndex
    reindex: function( zIndex ) {
      Layer.prototype.reindex.call( this, zIndex );
      
      if ( this.zIndex !== zIndex ) {
        this.svg.style.zIndex = zIndex;
        this.zIndex = zIndex;
      }
      return zIndex + 1;
    },
    
    pushClipShape: function( shape ) {
      // TODO: clipping
    },
    
    popClipShape: function() {
      // TODO: clipping
    },
    
    getSVGString: function() {
      // TODO: jQuery seems to be stripping namespaces, so figure that one out?
      return $( '<div>' ).append( this.$svg.clone() ).html();
      
      // also note:
      // var doc = document.implementation.createHTMLDocument("");
      // doc.write(html);
       
      // // You must manually set the xmlns if you intend to immediately serialize the HTML
      // // document to a string as opposed to appending it to a <foreignObject> in the DOM
      // doc.documentElement.setAttribute("xmlns", doc.documentElement.namespaceURI);
       
      // // Get well-formed markup
      // html = (new XMLSerializer).serializeToString(doc);
    },
    
    // TODO: note for DOM we can do https://developer.mozilla.org/en-US/docs/HTML/Canvas/Drawing_DOM_objects_into_a_canvas
    renderToCanvas: function( canvas, context, delayCounts ) {
      // temporarily put the full transform on the containing group so the rendering is correct (CSS transforms can take this away)
      this.applyTransform( this.baseTrail.getTransform(), this.g );
      
      if ( window.canvg ) {
        delayCounts.increment();
        
        // TODO: if we are using CSS3 transforms, run that here
        canvg( canvas, this.getSVGString(), {
          ignoreMouse: true,
          ignoreAnimation: true,
          ignoreDimensions: true,
          ignoreClear: true,
          renderCallback: function() {
            delayCounts.decrement();
          }
        } );
      } else {
        // will not work on Internet Explorer 9/10
        
        // TODO: very much not convinced that this is better than setting src of image
        var DOMURL = window.URL || window.webkitURL || window;
        var img = new Image();
        var raw = this.getSVGString();
        console.log( raw );
        var svg = new Blob( [ raw ] , { type: "image/svg+xml;charset=utf-8" } );
        var url = DOMURL.createObjectURL( svg );
        delayCounts.increment();
        img.onload = function() {
          context.drawImage( img, 0, 0 );
          // TODO: this loading is delayed!!! ... figure out a solution to potentially delay?
          DOMURL.revokeObjectURL( url );
          delayCounts.decrement();
        };
        img.src = url;
        
        throw new Error( 'this implementation hits Chrome bugs, won\'t work on IE9/10, etc. deprecated' );
      }
      
      // revert the transform damage that we did to our base group
      this.updateBaseTransform();
    },
    
    getName: function() {
      return 'svg';
    },
    
    /*---------------------------------------------------------------------------*
    * Events from Instances
    *----------------------------------------------------------------------------*/
    
    notifyVisibilityChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' notifyVisibilityChange: ' + instance.trail.toString() );
      var group = this.getGroupFromInstance( instance );
      if ( group ) {
        this.updateGroupVisibility( instance.getNode(), group );
      } else if ( this.baseNode !== this.scene ) {
        // if we are using a CSS transform (basically)
        this.updateGroupVisibility( this.baseNode, this.getGroupFromInstance( this.baseTrail.getInstance() ) );
      }
    },
    
    notifyOpacityChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' notifyOpacityChange: ' + instance.trail.toString() );
      var group = this.getGroupFromInstance( instance );
      if ( group ) {
        this.updateGroupOpacity( instance.getNode(), group );
      } else if ( this.baseNode !== this.scene ) {
        // if we are using a CSS transform (basically)
        this.updateGroupOpacity( this.baseNode, this.getGroupFromInstance( this.baseTrail.getInstance() ) );
      }
    },
    
    // only a painted trail under this layer
    notifyBeforeSelfChange: function( instance ) {
      // sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' notifyBeforeSelfChange: ' + instance.trail.toString() );
      // no-op, we don't need paint changes
    },
    
    notifyBeforeSubtreeChange: function( instance ) {
      // sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' notifyBeforeSubtreeChange: ' + instance.trail.toString() );
      // no-op, we don't need paint changes
    },
    
    // only a painted trail under this layer
    notifyDirtySelfPaint: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' notifyDirtySelfPaint: ' + instance.trail.toString() );
      var fragment = this.getFragmentFromInstance( instance );
      
      if ( fragment ) {
        var node = instance.getNode();
        
        if ( node.updateSVGFragment ) {
          // TODO: performance: don't re-do all of the fragment here! intelligent updating?
          node.updateSVGFragment( fragment );
        }
        if ( node.updateSVGDefs ) {
          // TODO: performance: don't re-do all of the defs here! intelligent updating?
          node.updateSVGDefs( this.svg, this.defs );
        }
      }
    },
    
    notifyDirtySubtreePaint: function( instance ) {
      // TODO: performance: this caused a 50% increase for the fast-svg transform steps, what were we missing?
      if ( instance.layer === this ) {
        this.notifyDirtySelfPaint( instance );
      }
    },
    
    notifyTransformChange: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' notifyTransformChange: ' + instance.trail.toString() );
      var node = instance.node;
      var trail = instance.trail;
      
      if ( trail.lastNode() === this.baseNode ) {
        // our trail points to the base node. handle this case as special
        this.markBaseTransformDirty();
      } else if ( _.contains( trail.nodes, this.baseNode ) ) { // TODO: performance: this linear scan looks slower than needed
        var group = this.idGroupMap[trail.getUniqueId()];
        
        // apply the transform to the group
        this.applyTransform( node.getTransform(), group );
      } else {
        // ancestor node changed a transform. rebuild the base transform
        this.markBaseTransformDirty();
      }
    },
    
    // only a painted trail under this layer (for now)
    notifyBoundsAccuracyChange: function( instance ) {
      // sceneryLayerLog && sceneryLayerLog( 'SVGLayer #' + this.id + ' notifyBoundsAccuracyChange: ' + instance.trail.toString() );
      // no-op, we don't care about bounds
    }
    
  } );
  
  return SVGLayer;
} );



// Copyright 2002-2013, University of Colorado

/**
 * An enumeration of different back-end technologies used for rendering. It also essentially
 * represents the API that nodes need to implement to be used with this specified back-end.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/Renderer',['require','SCENERY/scenery','SCENERY/layers/LayerType','SCENERY/layers/CanvasLayer','SCENERY/layers/DOMLayer','SCENERY/layers/SVGLayer'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  require( 'SCENERY/layers/LayerType' );
  require( 'SCENERY/layers/CanvasLayer' );
  require( 'SCENERY/layers/DOMLayer' );
  require( 'SCENERY/layers/SVGLayer' );
  
  // cached defaults
  var defaults = {};
  
  scenery.Renderer = function Renderer( layerConstructor, name, defaultOptions ) {
    this.layerConstructor = layerConstructor;
    this.name = name;
    this.defaultOptions = defaultOptions;
    
    this.defaultLayerType = this.createLayerType( {} ); // default options are handled in createLayerType
  };
  var Renderer = scenery.Renderer;
  
  Renderer.prototype = {
    constructor: Renderer,
    
    createLayerType: function( rendererOptions ) {
      return new scenery.LayerType( this.layerConstructor, this.name, this, _.extend( {}, this.defaultOptions, rendererOptions ) );
    }
  };
  
  Renderer.Canvas = new Renderer( scenery.CanvasLayer, 'canvas', {} );
  Renderer.DOM = new Renderer( scenery.DOMLayer, 'dom', {} );
  Renderer.SVG = new Renderer( scenery.SVGLayer, 'svg', {} );
  
  // add shortcuts for the default layer types
  scenery.CanvasDefaultLayerType = Renderer.Canvas.defaultLayerType;
  scenery.DOMDefaultLayerType    = Renderer.DOM.defaultLayerType;
  scenery.SVGDefaultLayerType    = Renderer.SVG.defaultLayerType;
  
  // and shortcuts so we can index in with shorthands like 'svg', 'dom', etc.
  Renderer.canvas = Renderer.Canvas;
  Renderer.dom = Renderer.DOM;
  Renderer.svg = Renderer.SVG;
  Renderer.webgl = Renderer.WebGL;
  
  return Renderer;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Mix-in for nodes that support a standard fill.
 *
 * TODO: pattern and gradient handling
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/Fillable',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  scenery.Fillable = function Fillable( type ) {
    var proto = type.prototype;
    
    // this should be called in the constructor to initialize
    proto.initializeFillable = function() {
      this._fill = null;
      
      var that = this;
      this._fillListener = function() {
        that.invalidatePaint(); // TODO: move this to invalidateFill?
        that.invalidateFill();
      };
    };
    
    proto.hasFill = function() {
      return this._fill !== null;
    };
    
    proto.getFill = function() {
      return this._fill;
    };
    
    proto.setFill = function( fill ) {
      if ( this.getFill() !== fill ) {
        var hasInstances = this._instances.length > 0;
        
        if ( hasInstances && this._fill && this._fill.removeChangeListener ) {
          this._fill.removeChangeListener( this._fillListener );
        }
        
        this._fill = fill;
        
        if ( hasInstances && this._fill && this._fill.addChangeListener ) {
          this._fill.addChangeListener( this._fillListener );
        }
        
        this.invalidatePaint();
        
        this.invalidateFill();
      }
      return this;
    };
    
    var superFirstInstanceAdded = proto.firstInstanceAdded;
    proto.firstInstanceAdded = function() {
      if ( this._fill && this._fill.addChangeListener ) {
        this._fill.addChangeListener( this._fillListener );
      }
      
      if ( superFirstInstanceAdded ) {
        superFirstInstanceAdded.call( this );
      }
    };
    
    var superLastInstanceRemoved = proto.lastInstanceRemoved;
    proto.lastInstanceRemoved = function() {
      if ( this._fill && this._fill.removeChangeListener ) {
        this._fill.removeChangeListener( this._fillListener );
      }
      
      if ( superLastInstanceRemoved ) {
        superLastInstanceRemoved.call( this );
      }
    };
    
    proto.beforeCanvasFill = function( wrapper ) {
      wrapper.setFillStyle( this._fill );
      if ( this._fill.transformMatrix ) {
        wrapper.context.save();
        this._fill.transformMatrix.canvasAppendTransform( wrapper.context );
      }
    };
    
    proto.afterCanvasFill = function( wrapper ) {
      if ( this._fill.transformMatrix ) {
        wrapper.context.restore();
      }
    };
    
    proto.getSVGFillStyle = function() {
      var style = 'fill: ';
      if ( !this._fill ) {
        // no fill
        style += 'none;';
      } else if ( this._fill.toCSS ) {
        // Color object fill
        style += this._fill.toCSS() + ';';
      } else if ( this._fill.getSVGDefinition ) {
        // reference the SVG definition with a URL
        style += 'url(#fill' + this.getId() + ');';
      } else {
        // plain CSS color
        style += this._fill + ';';
      }
      return style;
    };
    
    proto.isFillDOMCompatible = function() {
      // make sure we're not a pattern or gradient
      return !this._fill || !this._fill.getSVGDefinition;
    };
    
    proto.getCSSFill = function() {
      sceneryAssert && sceneryAssert( this.isFillDOMCompatible() );
      
      // if it's a Color object, get the corresponding CSS
      // 'transparent' will make us invisible if the fill is null
      return this._fill ? ( this._fill.toCSS ? this._fill.toCSS() : this._fill ) : 'transparent';
    };
    
    proto.addSVGFillDef = function( svg, defs ) {
      var fill = this.getFill();
      var fillId = 'fill' + this.getId();
      
      // add new definitions if necessary
      if ( fill && fill.getSVGDefinition ) {
        defs.appendChild( fill.getSVGDefinition( fillId ) );
      }
    };
    
    proto.removeSVGFillDef = function( svg, defs ) {
      var fillId = 'fill' + this.getId();
      
      // wipe away any old definition
      var oldFillDef = svg.getElementById( fillId );
      if ( oldFillDef ) {
        defs.removeChild( oldFillDef );
      }
    };
    
    proto.appendFillablePropString = function( spaces, result ) {
      if ( this._fill ) {
        if ( result ) {
          result += ',\n';
        }
        if ( typeof this._fill === 'string' ) {
          result += spaces + 'fill: \'' + this._fill + '\'';
        } else {
          result += spaces + 'fill: ' + this._fill.toString();
        }
      }
      
      return result;
    };
    
    // on mutation, set the fill parameter first
    proto._mutatorKeys = [ 'fill' ].concat( proto._mutatorKeys );
    
    Object.defineProperty( proto, 'fill', { set: proto.setFill, get: proto.getFill } );
    
    if ( !proto.invalidateFill ) {
      proto.invalidateFill = function() {
        // override if fill handling is necessary (TODO: mixins!)
      };
    }
  };
  var Fillable = scenery.Fillable;
  
  return Fillable;
} );



// Copyright 2002-2013, University of Colorado

/**
 * Mix-in for nodes that support a standard stroke.
 *
 * TODO: miterLimit handling
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/Strokable',['require','SCENERY/scenery','KITE/util/LineStyles'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  var LineStyles = require( 'KITE/util/LineStyles' );
  
  scenery.Strokable = function Strokable( type ) {
    var proto = type.prototype;
    
    // this should be called in the constructor to initialize
    proto.initializeStrokable = function() {
      this._stroke = null;
      this._lineDrawingStyles = new LineStyles();
      
      var that = this;
      this._strokeListener = function() {
        that.invalidatePaint(); // TODO: move this to invalidateStroke?
        that.invalidateStroke();
      };
    };
    
    proto.hasStroke = function() {
      return this._stroke !== null;
    };
    
    // TODO: setting these properties looks like a good candidate for refactoring to lessen file size
    proto.getLineWidth = function() {
      return this._lineDrawingStyles.lineWidth;
    };
    
    proto.setLineWidth = function( lineWidth ) {
      if ( this.getLineWidth() !== lineWidth ) {
        this.markOldSelfPaint(); // since the previous line width may have been wider
        
        this._lineDrawingStyles.lineWidth = lineWidth;
        
        this.invalidateStroke();
      }
      return this;
    };
    
    proto.getLineCap = function() {
      return this._lineDrawingStyles.lineCap;
    };
    
    proto.setLineCap = function( lineCap ) {
      if ( this._lineDrawingStyles.lineCap !== lineCap ) {
        this.markOldSelfPaint();
        
        this._lineDrawingStyles.lineCap = lineCap;
        
        this.invalidateStroke();
      }
      return this;
    };
    
    proto.getLineJoin = function() {
      return this._lineDrawingStyles.lineJoin;
    };
    
    proto.setLineJoin = function( lineJoin ) {
      if ( this._lineDrawingStyles.lineJoin !== lineJoin ) {
        this.markOldSelfPaint();
        
        this._lineDrawingStyles.lineJoin = lineJoin;
        
        this.invalidateStroke();
      }
      return this;
    };
    
    proto.getLineDash = function() {
      return this._lineDrawingStyles.lineDash;
    };
    
    proto.setLineDash = function( lineDash ) {
      if ( this._lineDrawingStyles.lineDash !== lineDash ) {
        this.markOldSelfPaint();
        
        this._lineDrawingStyles.lineDash = lineDash || [];
        
        this.invalidateStroke();
      }
      return this;
    };
    
    proto.getLineDashOffset = function() {
      return this._lineDrawingStyles.lineDashOffset;
    };
    
    proto.setLineDashOffset = function( lineDashOffset ) {
      if ( this._lineDrawingStyles.lineDashOffset !== lineDashOffset ) {
        this.markOldSelfPaint();
        
        this._lineDrawingStyles.lineDashOffset = lineDashOffset;
        
        this.invalidateStroke();
      }
      return this;
    };
    
    proto.setLineStyles = function( lineStyles ) {
      // TODO: since we have been using lineStyles as mutable for now, lack of change check is good here?
      this.markOldSelfPaint();
      
      this._lineDrawingStyles = lineStyles;
      this.invalidateStroke();
      return this;
    };
    
    proto.getLineStyles = function() {
      return this._lineDrawingStyles;
    };
    
    proto.getStroke = function() {
      return this._stroke;
    };
    
    proto.setStroke = function( stroke ) {
      if ( this.getStroke() !== stroke ) {
        // since this can actually change the bounds, we need to handle a few things differently than the fill
        this.markOldSelfPaint();
        
        var hasInstances = this._instances.length > 0;
        
        if ( hasInstances && this._stroke && this._stroke.removeChangeListener ) {
          this._stroke.removeChangeListener( this._strokeListener );
        }
        
        this._stroke = stroke;
        
        if ( hasInstances && this._stroke && this._stroke.addChangeListener ) {
          this._stroke.addChangeListener( this._strokeListener );
        }
        
        this.invalidateStroke();
      }
      return this;
    };
    
    var superFirstInstanceAdded = proto.firstInstanceAdded;
    proto.firstInstanceAdded = function() {
      if ( this._stroke && this._stroke.addChangeListener ) {
        this._stroke.addChangeListener( this._strokeListener );
      }
      
      if ( superFirstInstanceAdded ) {
        superFirstInstanceAdded.call( this );
      }
    };
    
    var superLastInstanceRemoved = proto.lastInstanceRemoved;
    proto.lastInstanceRemoved = function() {
      if ( this._stroke && this._stroke.removeChangeListener ) {
        this._stroke.removeChangeListener( this._strokeListener );
      }
      
      if ( superLastInstanceRemoved ) {
        superLastInstanceRemoved.call( this );
      }
    };
    
    proto.beforeCanvasStroke = function( wrapper ) {
      // TODO: is there a better way of not calling so many things on each stroke?
      wrapper.setStrokeStyle( this._stroke );
      wrapper.setLineWidth( this.getLineWidth() );
      wrapper.setLineCap( this.getLineCap() );
      wrapper.setLineJoin( this.getLineJoin() );
      wrapper.setLineDash( this.getLineDash() );
      wrapper.setLineDashOffset( this.getLineDashOffset() );
      if ( this._stroke.transformMatrix ) {
        wrapper.context.save();
        this._stroke.transformMatrix.canvasAppendTransform( wrapper.context );
      }
    };
    
    proto.afterCanvasStroke = function( wrapper ) {
      if ( this._stroke.transformMatrix ) {
        wrapper.context.restore();
      }
    };
    
    proto.getSVGStrokeStyle = function() {
      if ( !this._stroke ) {
        // no stroke
        return 'stroke: none;';
      }
      
      var style = 'stroke: ';
      if ( this._stroke.toCSS ) {
        // Color object stroke
        style += this._stroke.toCSS() + ';';
      } else if ( this._stroke.getSVGDefinition ) {
        // reference the SVG definition with a URL
        style += 'url(#stroke' + this.getId() + ');';
      } else {
        // plain CSS color
        style += this._stroke + ';';
      }
      
      // TODO: don't include unnecessary directives? - is it worth any branching cost?
      style += 'stroke-width: ' + this.getLineWidth() + ';';
      style += 'stroke-linecap: ' + this.getLineCap() + ';';
      style += 'stroke-linejoin: ' + this.getLineJoin() + ';';
      if ( this.getLineDash().length ) {
        style += 'stroke-dasharray: ' + this.getLineDash().join( ',' ) + ';';
        style += 'stroke-dashoffset: ' + this.getLineDashOffset() + ';';
      }
      
      return style;
    };
    
    proto.addSVGStrokeDef = function( svg, defs ) {
      var stroke = this.getStroke();
      var strokeId = 'stroke' + this.getId();
      
      // add new definitions if necessary
      if ( stroke && stroke.getSVGDefinition ) {
        defs.appendChild( stroke.getSVGDefinition( strokeId ) );
      }
    };
    
    proto.removeSVGStrokeDef = function( svg, defs ) {
      var strokeId = 'stroke' + this.getId();
      
      // wipe away any old definition
      var oldStrokeDef = svg.getElementById( strokeId );
      if ( oldStrokeDef ) {
        defs.removeChild( oldStrokeDef );
      }
    };
    
    proto.appendStrokablePropString = function( spaces, result ) {
      var self = this;
      
      function addProp( key, value, nowrap ) {
        if ( result ) {
          result += ',\n';
        }
        if ( !nowrap && typeof value === 'string' ) {
          result += spaces + key + ': \'' + value + '\'';
        } else {
          result += spaces + key + ': ' + value;
        }
      }
      
      if ( this._stroke ) {
        var defaultStyles = new LineStyles();
        if ( typeof this._stroke === 'string' ) {
          addProp( 'stroke', this._stroke );
        } else {
          addProp( 'stroke', this._stroke.toString(), true );
        }
        
        _.each( [ 'lineWidth', 'lineCap', 'lineJoin', 'lineDashOffset' ], function( prop ) {
          if ( self[prop] !== defaultStyles[prop] ) {
            addProp( prop, self[prop] );
          }
        } );
        
        if ( this.lineDash.length ) {
          addProp( 'lineDash', JSON.stringify( this.lineDash ), true );
        }
      }
      
      return result;
    };
    
    // on mutation, set the stroke parameters first since they may affect the bounds (and thus later operations)
    proto._mutatorKeys = [ 'stroke', 'lineWidth', 'lineCap', 'lineJoin', 'lineDash', 'lineDashOffset' ].concat( proto._mutatorKeys );
    
    // TODO: miterLimit support?
    Object.defineProperty( proto, 'stroke', { set: proto.setStroke, get: proto.getStroke } );
    Object.defineProperty( proto, 'lineWidth', { set: proto.setLineWidth, get: proto.getLineWidth } );
    Object.defineProperty( proto, 'lineCap', { set: proto.setLineCap, get: proto.getLineCap } );
    Object.defineProperty( proto, 'lineJoin', { set: proto.setLineJoin, get: proto.getLineJoin } );
    Object.defineProperty( proto, 'lineDash', { set: proto.setLineDash, get: proto.getLineDash } );
    Object.defineProperty( proto, 'lineDashOffset', { set: proto.setLineDashOffset, get: proto.getLineDashOffset } );
    
    if ( !proto.invalidateStroke ) {
      proto.invalidateStroke = function() {
        // override if stroke handling is necessary (TODO: mixins!)
      };
    }
  };
  var Strokable = scenery.Strokable;
  
  return Strokable;
} );



// Copyright 2002-2013, University of Colorado

/**
 * A Path draws a Shape with a specific type of fill and stroke.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/Path',['require','PHET_CORE/inherit','KITE/Shape','SCENERY/scenery','SCENERY/nodes/Node','SCENERY/layers/Renderer','SCENERY/nodes/Fillable','SCENERY/nodes/Strokable','SCENERY/util/Util'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Shape = require( 'KITE/Shape' );
  
  var scenery = require( 'SCENERY/scenery' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Renderer = require( 'SCENERY/layers/Renderer' );
  var Fillable = require( 'SCENERY/nodes/Fillable' );
  var Strokable = require( 'SCENERY/nodes/Strokable' );
  var objectCreate = require( 'SCENERY/util/Util' ).objectCreate;
  
  scenery.Path = function Path( shape, options ) {
    // TODO: consider directly passing in a shape object (or at least handling that case)
    this._shape = null;

    // ensure we have a parameter object
    options = options || {};
    
    this.initializeFillable();
    this.initializeStrokable();

    Node.call( this );
    this.setShape( shape );
    this.mutate( options );
  };
  var Path = scenery.Path;
  
  inherit( Node, Path, {
    // sets the shape drawn, or null to remove the shape
    setShape: function( shape ) {
      if ( this._shape !== shape ) {
        if ( typeof shape === 'string' ) {
          // be content with setShape always invalidating the shape?
          shape = new Shape( shape );
        }
        this._shape = shape;
        this.invalidateShape();
      }
      return this;
    },
    
    getShape: function() {
      return this._shape;
    },
    
    invalidateShape: function() {
      this.markOldSelfPaint();
      
      if ( this.hasShape() ) {
        this.invalidateSelf( this.computeShapeBounds() );
        this.invalidatePaint();
      }
    },
    
    // separated out, so that we can override this with a faster version in Rectangle. includes the Stroke, if any
    computeShapeBounds: function() {
      return this._stroke ? this._shape.computeBounds( this._lineDrawingStyles ) : this._shape.bounds;
    },
    
    // hook stroke mixin changes to invalidation
    invalidateStroke: function() {
      this.invalidateShape();
    },
    
    hasShape: function() {
      return this._shape;
    },
    
    paintCanvas: function( wrapper ) {
      var context = wrapper.context;
      
      if ( this.hasShape() ) {
        // TODO: fill/stroke delay optimizations?
        context.beginPath();
        this._shape.writeToContext( context );

        if ( this._fill ) {
          this.beforeCanvasFill( wrapper ); // defined in Fillable
          context.fill();
          this.afterCanvasFill( wrapper ); // defined in Fillable
        }
        if ( this._stroke ) {
          this.beforeCanvasStroke( wrapper ); // defined in Strokable
          context.stroke();
          this.afterCanvasStroke( wrapper ); // defined in Strokable
        }
      }
    },
    
    paintWebGL: function( state ) {
      throw new Error( 'Path.prototype.paintWebGL unimplemented' );
    },
    
    // svg element, the <defs> block, and the associated group for this node's transform
    createSVGFragment: function( svg, defs, group ) {
      return document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
    },
    
    updateSVGFragment: function( path ) {
      var svgPath = this.hasShape() ? this._shape.getSVGPath() : "";
      
      // temporary workaround for https://bugs.webkit.org/show_bug.cgi?id=78980
      // and http://code.google.com/p/chromium/issues/detail?id=231626 where even removing
      // the attribute can cause this bug
      if ( !svgPath ) { svgPath = 'M0 0'; }
      
      if ( svgPath ) {
        // only set the SVG path if it's not the empty string
        path.setAttribute( 'd', svgPath );
      } else if ( path.hasAttribute( 'd' ) ) {
        path.removeAttribute( 'd' );
      }
      
      path.setAttribute( 'style', this.getSVGFillStyle() + this.getSVGStrokeStyle() );
    },
    
    // support patterns, gradients, and anything else we need to put in the <defs> block
    updateSVGDefs: function( svg, defs ) {
      // remove old definitions if they exist
      this.removeSVGDefs( svg, defs );
      
      // add new ones if applicable
      this.addSVGFillDef( svg, defs );
      this.addSVGStrokeDef( svg, defs );
    },
    
    // cleans up references created with udpateSVGDefs()
    removeSVGDefs: function( svg, defs ) {
      this.removeSVGFillDef( svg, defs );
      this.removeSVGStrokeDef( svg, defs );
    },
    
    isPainted: function() {
      return true;
    },
    
    // override for computation of whether a point is inside the self content
    // point is considered to be in the local coordinate frame
    containsPointSelf: function( point ) {
      if ( !this.hasShape() ) {
        return false;
      }
      
      var result = this._shape.containsPoint( point );
      
      // also include the stroked region in the hit area if applicable
      if ( !result && this._includeStrokeInHitRegion && this.hasStroke() ) {
        result = this._shape.getStrokedShape( this._lineDrawingStyles ).containsPoint( point );
      }
      return result;
    },
    
    // whether this node's self intersects the specified bounds, in the local coordinate frame
    intersectsBoundsSelf: function( bounds ) {
      // TODO: should a shape's stroke be included?
      return this.hasShape() ? this._shape.intersectsBounds( bounds ) : false;
    },
    
    set shape( value ) { this.setShape( value ); },
    get shape() { return this.getShape(); },
    
    getBasicConstructor: function( propLines ) {
      return 'new scenery.Path( ' + this._shape.toString() + ', {' + propLines + '} )';
    },
    
    getPropString: function( spaces, includeChildren ) {
      var result = Node.prototype.getPropString.call( this, spaces, includeChildren );
      result = this.appendFillablePropString( spaces, result );
      result = this.appendStrokablePropString( spaces, result );
      return result;
    }
  } );
  
  Path.prototype._mutatorKeys = [ 'shape' ].concat( Node.prototype._mutatorKeys );
  
  Path.prototype._supportedRenderers = [ Renderer.Canvas, Renderer.SVG ];
  
  // mix in fill/stroke handling code. for now, this is done after 'shape' is added to the mutatorKeys so that stroke parameters
  // get set first
  /* jshint -W064 */
  Fillable( Path );
  Strokable( Path );
  
  return Path;
} );



// Copyright 2002-2013, University of Colorado Boulder

/**
 * HBox arranges the child nodes horizontally, and they can be centered, left or right justified.
 * Horizontal spacing can be set as a constant or a function which depends on the adjacent nodes.
 * TODO: add an option (not enabled by default) to update layout when children or children bounds change
 *
 * @author Sam Reid
 */

define('SCENERY/nodes/HBox',['require','SCENERY/scenery','SCENERY/nodes/Node','SCENERY/util/Util'], function( require ) {
  

  var scenery = require( 'SCENERY/scenery' );
  var Node = require( 'SCENERY/nodes/Node' );
  var objectCreate = require( 'SCENERY/util/Util' ).objectCreate; // i.e. Object.create

  /**
   *
   * @param options Same as Node.constructor.options with the following additions:
   *
   * spacing: can be a number or a function.  If a number, then it will be the vertical spacing between each object.
   *              If a function, then the function will have the signature function(top,bottom){} which returns the spacing between adjacent pairs of items.
   * align:   How to line up the items horizontally.  One of 'center', 'top' or 'bottom'.  Defaults to 'center'.
   *
   * @constructor
   */
  scenery.HBox = function HBox( options ) {
    // ensure we have a parameter object
    this.options = options = _.extend( {
      // defaults
      spacing: function() { return 0; },
      align: 'center'
    }, options );

    if ( typeof options.spacing === 'number' ) {
      var spacingConstant = options.spacing;
      options.spacing = function() { return spacingConstant; };
    }

    Node.call( this, options );
    this.updateLayout();

    //TODO: perhaps not all options must be passed to Node.call AND this.mutate.  On 7/17/2013, layout problems occurred when using only mutate() or only Node.call(this,options)
    //See VBox.js
    if ( options ) {
      this.mutate( options );
    }
  };
  var HBox = scenery.HBox;

  HBox.prototype = objectCreate( Node.prototype );

  HBox.prototype.updateLayout = function() {
    var minY = _.min( _.map( this.children, function( child ) {return child.top;} ) );
    var maxY = _.max( _.map( this.children, function( child ) {return child.top + child.height;} ) );
    var centerY = (maxY + minY) / 2;

    //Start at x=0 in the coordinate frame of this node.  Not possible to set this through the spacing option, instead just set it with the {y:number} option.
    var x = 0;
    for ( var i = 0; i < this.children.length; i++ ) {
      var child = this.children[i];
      child.left = x;

      //Set the position horizontally
      if ( this.options.align === 'top' ) {
        child.top = minY;
      }
      else if ( this.options.align === 'bottom' ) {
        child.bottom = maxY;
      }
      else {//default to center
        child.centerY = centerY;
      }

      //Move to the next vertical position.
      x += child.width + this.options.spacing( child, this.children[i + 1] );
    }
  };
  HBox.prototype.constructor = HBox;

  return HBox;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * VBox arranges the child nodes vertically, and they can be centered, left or right justified.
 * Vertical spacing can be set as a constant or a function which depends on the adjacent nodes.
 * TODO: add an option (not enabled by default) to update layout when children or children bounds change
 *
 * @author Sam Reid
 */

define('SCENERY/nodes/VBox',['require','SCENERY/scenery','SCENERY/nodes/Node','SCENERY/util/Util'], function( require ) {
  

  var scenery = require( 'SCENERY/scenery' );
  var Node = require( 'SCENERY/nodes/Node' );
  var objectCreate = require( 'SCENERY/util/Util' ).objectCreate; // i.e. Object.create

  /**
   *
   * @param options Same as Node.constructor.options with the following additions:
   *
   * spacing: can be a number or a function.  If a number, then it will be the vertical spacing between each object.
   *              If a function, then the function will have the signature function(top,bottom){} which returns the spacing between adjacent pairs of items.
   * align:   How to line up the items horizontally.  One of 'center', 'left' or 'right'.  Defaults to 'center'.
   *
   * @constructor
   */
  scenery.VBox = function VBox( options ) {
    // ensure we have a parameter object
    this.options = options = _.extend( {
      // defaults
      spacing: function() { return 0; },
      align: 'center'
    }, options );

    if ( typeof options.spacing === 'number' ) {
      var spacingConstant = options.spacing;
      options.spacing = function() { return spacingConstant; };
    }

    Node.call( this, options );
    this.updateLayout();

    //TODO: perhaps not all options must be passed to Node.call AND this.mutate.  On 7/17/2013, layout problems occurred when using only mutate() or only Node.call(this,options)
    //See HBox.js
    if ( options ) {
      this.mutate( options );
    }
  };
  var VBox = scenery.VBox;

  VBox.prototype = objectCreate( Node.prototype );

  VBox.prototype.updateLayout = function() {
    var minX = _.min( _.map( this.children, function( child ) {return child.left;} ) );
    var maxX = _.max( _.map( this.children, function( child ) {return child.left + child.width;} ) );
    var centerX = (maxX + minX) / 2;

    //Start at y=0 in the coordinate frame of this node.  Not possible to set this through the spacing option, instead just set it with the {y:number} option.
    var y = 0;
    for ( var i = 0; i < this.children.length; i++ ) {
      var child = this.children[i];
      child.top = y;

      //Set the position horizontally
      if ( this.options.align === 'left' ) {
        child.left = minX;
      }
      else if ( this.options.align === 'right' ) {
        child.right = maxX;
      }
      else {//default to center
        child.centerX = centerX;
      }

      //Move to the next vertical position.
      y += child.height + this.options.spacing( child, this.children[i + 1] );
    }
  };
  VBox.prototype.constructor = VBox;

  return VBox;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Escaping of HTML content that will be placed in the body, inside an element as a node.
 *
 * This is NOT for escaping something in other HTML contexts, for example as an attribute value
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */
define('PHET_CORE/escapeHTML',['require'], function( require ) {
  
  
  return function escapeHTML( str ) {
    // see https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
    // HTML Entity Encoding
    return str.replace( /&/g, '&amp;' )
              .replace( /</g, '&lt;' )
              .replace( />/g, '&gt;' )
              .replace( /\"/g, '&quot;' )
              .replace( /\'/g, '&#x27;' )
              .replace( /\//g, '&#x2F;' );
  };
} );

// Copyright 2002-2013, University of Colorado

/**
 * Font handling for text drawing
 *
 * Examples:
 * new scenery.Font().font                      // "10px sans-serif" (the default)
 * new scenery.Font( { family: 'serif' } ).font // "10px serif"
 * new scenery.Font( { weight: 'bold' } ).font  // "bold 10px sans-serif"
 * new scenery.Font( { size: 16 } ).font        // "16px sans-serif"
 * var font = new scenery.Font( {
 *   family: '"Times New Roman", serif',
 *   style: 'italic',
 *   lineHeight: 10
 * } );
 * font.font;                                   // "italic 10px/10 'Times New Roman', serif"
 * font.family;                                 // "'Times New Roman', serif"
 * font.weight;                                 // 400 (the default)
 *
 * Useful specs:
 * http://www.w3.org/TR/css3-fonts/
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/Font',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  // constants used for detection (since styles/variants/weights/stretches can be mixed in the preamble of the shorthand string)
  var styles = [ 'normal', 'italic', 'oblique' ];
  var variants = [ 'normal', 'small-caps' ];
  var weights = [ 'normal', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '700', '800', '900' ];
  var stretches = [ 'normal', 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded' ];
  
  // size constants used for detection
  var sizes = [ 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'larger', 'smaller' ];
  
  function castSize( size ) {
    if ( typeof size === 'number' ) {
      return size + 'px'; // add the pixels suffix by default for numbers
    } else {
      return size; // assume that it's a valid to-spec string
    }
  }
  
  scenery.Font = function Font( options ) {
    // options from http://www.w3.org/TR/css3-fonts/
    this._style = 'normal';      // normal | italic | oblique
    this._variant = 'normal';    // normal | small-caps
    this._weight = 'normal';     // normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
    this._stretch = 'normal';    // normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded
    this._size = '10px';         // <absolute-size> | <relative-size> | <length> | <percentage> -- unitless number interpreted as px. absolute suffixes: cm, mm, in, pt, pc, px. relative suffixes: em, ex, ch, rem, vw, vh, vmin, vmax.
    this._lineHeight = 'normal'; // normal | <number> | <length> | <percentage> -- NOTE: Canvas spec forces line-height to normal
    this._family = 'sans-serif'; // comma-separated list of families, including generic families (serif, sans-serif, cursive, fantasy, monospace). ideally escape with double-quotes
    
    // font  [ [ <‘font-style’> || <font-variant-css21> || <‘font-weight’> || <‘font-stretch’> ]? <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] | caption | icon | menu | message-box | small-caption | status-bar
    // <font-variant-css21> = [normal | small-caps]
    
    var type = typeof options;
    if ( type === 'string' ) {
      // parse a somewhat proper CSS3 form (not guaranteed to handle it precisely the same as browsers yet)
      
      // split based on whitespace allowed by CSS spec (more restrictive than regular regexp whitespace)
      var tokens = _.filter( options.split( /[\x09\x0A\x0C\x0D\x20]/ ), function( token ) { return token.length > 0; } );
      
      // pull tokens out until we reach something that doesn't match. that must be the font size (according to spec)
      for ( var i = 0; i < tokens.length; i++ ) {
        var token = tokens[i];
        if ( token === 'normal' ) {
          // nothing has to be done, everything already normal as default
        } else if ( _.contains( styles, token ) ) {
          sceneryAssert && sceneryAssert( this._style === 'normal', 'Style cannot be applied twice. Already set to "' + this._style + '", attempt to replace with "' + token + '"' );
          this._style = token;
        } else if ( _.contains( variants, token ) ) {
          sceneryAssert && sceneryAssert( this._variant === 'normal', 'Variant cannot be applied twice. Already set to "' + this._variant + '", attempt to replace with "' + token + '"' );
          this._variant = token;
        } else if ( _.contains( weights, token ) ) {
          sceneryAssert && sceneryAssert( this._weight === 'normal', 'Weight cannot be applied twice. Already set to "' + this._weight + '", attempt to replace with "' + token + '"' );
          this._weight = token;
        } else if ( _.contains( stretches, token ) ) {
          sceneryAssert && sceneryAssert( this._stretch === 'normal', 'Stretch cannot be applied twice. Already set to "' + this._stretch + '", attempt to replace with "' + token + '"' );
          this._stretch = token;
        } else {
          // not a style/variant/weight/stretch, must be a font size, possibly with an included line-height
          var subtokens = token.split( /\// ); // extract font size from any line-height
          this._size = subtokens[0];
          if ( subtokens[1] ) {
            this._lineHeight = subtokens[1];
          }
          // all future tokens are guaranteed to be part of the font-family if it is given according to spec
          this._family = tokens.slice( i + 1 ).join( ' ' );
          break;
        }
      }
    } else if ( type === 'object' ) {
      if ( options.style !== undefined ) {
        this._style = options.style;
      }
      if ( options.variant !== undefined ) {
        this._variant = options.variant;
      }
      if ( options.weight !== undefined ) {
        this._weight = '' + options.weight; // cast it to a string explicitly
      }
      if ( options.stretch !== undefined ) {
        this._stretch = options.stretch;
      }
      if ( options.size !== undefined ) {
        this._size = castSize( options.size );
      }
      if ( options.lineHeight !== undefined ) {
        this._lineHeight = options.lineHeight;
      }
      if ( options.family !== undefined ) {
        this._family = options.family;
      }
    }
    
    // sanity checks to prevent errors in interpretation or in the font shorthand usage
    sceneryAssert && sceneryAssert( typeof this._style === 'string' &&
                                    _.contains( styles, this._style ),
                                    'Font style must be one of "normal", "italic", or "oblique"' );
    sceneryAssert && sceneryAssert( typeof this._variant === 'string' &&
                                    _.contains( variants, this._variant ),
                                    'Font variant must be "normal" or "small-caps"' );
    sceneryAssert && sceneryAssert( typeof this._weight === 'string' &&
                                    _.contains( weights, this._weight ),
                                    'Font weight must be one of "normal", "bold", "bolder", "lighter", "100", "200", "300", "400", "500", "600", "700", "800", or "900"' );
    sceneryAssert && sceneryAssert( typeof this._stretch === 'string' &&
                                    _.contains( stretches, this._stretch ),
                                    'Font stretch must be one of "normal", "ultra-condensed", "extra-condensed", "condensed", "semi-condensed", "semi-expanded", "expanded", "extra-expanded", or "ultra-expanded"' );
    sceneryAssert && sceneryAssert( typeof this._size === 'string' &&
                                    !_.contains( [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ], this._size[this._size.length - 1] ),
                                    'Font size must be either passed as a number (not a string, interpreted as px), or must contain a suffix for percentage, absolute or relative units, or an explicit size constant' );
    sceneryAssert && sceneryAssert( typeof this._lineHeight === 'string' );
    sceneryAssert && sceneryAssert( typeof this._family === 'string' );
    
    // initialize the shorthand font property (stored as _font)
    this._font = this.computeShorthand();
    
    phetAllocation && phetAllocation( 'Font' );
  };
  var Font = scenery.Font;
  
  Font.prototype = {
    constructor: Font,
    
    getFont:       function() { return this._font; },
    getStyle:      function() { return this._style; },
    getVariant:    function() { return this._variant; },
    getWeight:     function() { return this._weight; },
    getStretch:    function() { return this._stretch; },
    getSize:       function() { return this._size; },
    getLineHeight: function() { return this._lineHeight; },
    getFamily:     function() { return this._family; },
    
    get font()       { return this.getFont(); },
    get style()      { return this.getStyle(); },
    get variant()    { return this.getVariant(); },
    get weight()     { return this.getWeight(); },
    get stretch()    { return this.getStretch(); },
    get size()       { return this.getSize(); },
    get lineHeight() { return this.getLineHeight(); },
    get family()     { return this.getFamily(); },
    
    copy: function( options ) {
      return new Font( _.extend( {
        style: this._style,
        variant: this._variant,
        weight: this._weight,
        stretch: this._stretch,
        size: this._size,
        lineHeight: this._lineHeight,
        family: this._family
      }, options ) );
    },
    
    computeShorthand: function() {
      var ret = '';
      if ( this._style !== 'normal' ) { ret += this._style + ' '; }
      if ( this._variant !== 'normal' ) { ret += this._variant + ' '; }
      if ( this._weight !== 'normal' ) { ret += this._weight + ' '; }
      if ( this._stretch !== 'normal' ) { ret += this._stretch + ' '; }
      ret += this._size;
      if ( this._lineHeight !== 'normal' ) { ret += '/' + this._lineHeight; }
      ret += ' ' + this._family;
      return ret;
    },
    
    toCSS: function() {
      return this.getFont();
    }
  };
  
  Font.DEFAULT = new Font();
  
  return Font;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Text
 *
 * TODO: newlines (multiline)
 * TODO: htmlText support (and DOM renderer)
 * TODO: don't get bounds until the Text node is fully mutated?
 * TODO: remove some support for centering, since Scenery's Node already handles that better?
 *
 * Useful specs:
 * http://www.w3.org/TR/css3-text/
 * http://www.w3.org/TR/css3-fonts/
 * http://www.w3.org/TR/SVG/text.html
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/Text',['require','PHET_CORE/inherit','PHET_CORE/escapeHTML','DOT/Bounds2','DOT/Matrix3','SCENERY/scenery','SCENERY/nodes/Node','SCENERY/layers/Renderer','SCENERY/nodes/Fillable','SCENERY/nodes/Strokable','SCENERY/util/Util','SCENERY/util/Font','SCENERY/util/Util'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var escapeHTML = require( 'PHET_CORE/escapeHTML' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Matrix3 = require( 'DOT/Matrix3' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  var Node = require( 'SCENERY/nodes/Node' ); // inherits from Node
  var Renderer = require( 'SCENERY/layers/Renderer' );
  var Fillable = require( 'SCENERY/nodes/Fillable' );
  var Strokable = require( 'SCENERY/nodes/Strokable' );
  var objectCreate = require( 'SCENERY/util/Util' ).objectCreate; // i.e. Object.create
  require( 'SCENERY/util/Font' );
  require( 'SCENERY/util/Util' ); // for canvasAccurateBounds and CSS transforms
  
  // set up the container and text for testing text bounds quickly (using approximateSVGBounds)
  var svgTextSizeContainer = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
  svgTextSizeContainer.setAttribute( 'width', '2' );
  svgTextSizeContainer.setAttribute( 'height', '2' );
  svgTextSizeContainer.setAttribute( 'style', 'display: hidden; pointer-events: none; position: absolute; left: -65535; right: -65535;' ); // so we don't flash it in a visible way to the user
  var svgTextSizeElement = document.createElementNS( 'http://www.w3.org/2000/svg', 'text' );
  svgTextSizeElement.appendChild( document.createTextNode( '' ) );
  svgTextSizeContainer.appendChild( svgTextSizeElement );
  
  // SVG bounds seems to be malfunctioning for Safari 5. Since we don't have a reproducible test machine for
  // fast iteration, we'll guess the user agent and use DOM bounds instead of SVG.
  // Hopefully the two contraints rule out any future Safari versions (fairly safe, but not impossible!)
  var useDOMAsFastBounds = window.navigator.userAgent.indexOf( 'like Gecko) Version/5' ) !== -1 &&
                           window.navigator.userAgent.indexOf( 'Safari/' ) !== -1;
  
  var hybridTextNode; // a node that is used to measure SVG text top/height for hybrid caching purposes
  var initializingHybridTextNode = false;
  
  scenery.Text = function Text( text, options ) {
    this._text         = '';                   // filled in with mutator
    this._font         = scenery.Font.DEFAULT; // default font, usually 10px sans-serif
    this._direction    = 'ltr';                // ltr, rtl, inherit -- consider inherit deprecated, due to how we compute text bounds in an off-screen canvas
    this._boundsMethod = 'hybrid';             // fast (SVG/DOM, no canvas rendering allowed), fastCanvas (SVG/DOM, canvas rendering allowed without dirty regions),
                                               // accurate (Canvas accurate recursive), or hybrid (cache SVG height, use canvas measureText for width)
    
    // whether the text is rendered as HTML or not. if defined (in a subtype constructor), use that value instead
    this._isHTML = this._isHTML === undefined ? false : this._isHTML;
    
    // we will dynamically change renderers, so they are initialized per-instance instead of per-type
    this._supportedRenderers = [ Renderer.Canvas, Renderer.SVG, Renderer.DOM ];
    
    var thisFont = this;
    
    // ensure we have a parameter object
    options = options || {};
    
    // default to black filled text
    if ( options.fill === undefined ) {
      options.fill = '#000000';
    }
    
    if ( text !== undefined ) {
      // set the text parameter so that setText( text ) is effectively called in the mutator from the super call
      options.text = text;
    }
    
    this.initializeStrokable();
    
    Node.call( this, options );
    
    this.updateTextFlags();
  };
  var Text = scenery.Text;
  
  inherit( Node, Text, {
    domUpdateTransformOnRepaint: true, // since we have to integrate the baseline offset into the CSS transform, signal to DOMLayer
    
    setText: function( text ) {
      if ( text !== this._text ) {
        this._text = text;
        this.invalidateText();
      }
      return this;
    },
    
    getText: function() {
      return this._text;
    },
    
    setBoundsMethod: function( method ) {
      sceneryAssert && sceneryAssert( method === 'fast' || method === 'fastCanvas' || method === 'accurate' || method === 'hybrid', 'Unknown Text boundsMethod' );
      if ( method !== this._boundsMethod ) {
        this._boundsMethod = method;
        this.updateTextFlags();
        this.dispatchEvent( 'boundsAccuracy', { node: this } ); // TODO: consider standardizing this, or attaching listeners in a different manner?
        this.invalidateText();
      }
      return this;
    },
    
    getBoundsMethod: function() {
      return this._boundsMethod;
    },
    
    updateTextFlags: function() {
      var thisText = this;
      this.boundsInaccurate = this._boundsMethod !== 'accurate';
      
      var renderersChanged = false;
      function check( predicateValue, renderer ) {
        var inSupportedRenderers = _.contains( thisText._supportedRenderers, renderer );
        if ( predicateValue !== inSupportedRenderers ) {
          renderersChanged = true;
          if ( predicateValue ) {
            // add the renderer
            thisText._supportedRenderers.push( renderer );
          } else {
            // remove the renderer
            thisText._supportedRenderers.splice( _.indexOf( thisText._supportedRenderers, renderer ), 1 );
            if ( thisText.renderer === renderer ) {
              // our set renderer is incompatible. set to null to disable this. TODO: investigate rendering system to prevent overrides like this?
              thisText.renderer = null;
              
              // for now, error out
              throw new Error( 'The explicitly specified Text renderer: ' + renderer.name + ' is not supported by this operation (probably invalid stroke, fill, or boundsMethod)' );
            }
          }
        }
      }
      
      check( this._boundsMethod !== 'fast' && !this._isHTML, Renderer.Canvas );
      check( !this._isHTML, Renderer.SVG );
      check( !this.hasStroke() && this.isFillDOMCompatible(), Renderer.DOM );
      
      if ( this._supportedRenderers.length === 0 ) {
        throw new Error( 'No renderers are able to support this Text node (probably HTML text with a stroke or incompatible fill)' );
      }
      
      if ( renderersChanged ) {
        this.markLayerRefreshNeeded();
      }
    },
    
    invalidateText: function() {
      // investigate http://mudcu.be/journal/2011/01/html5-typographic-metrics/
      if ( this._isHTML || ( useDOMAsFastBounds && this._boundsMethod !== 'accurate' ) ) {
        this.invalidateSelf( this.approximateDOMBounds() );
      } else if ( this._boundsMethod === 'hybrid' ) {
        this.invalidateSelf( this.approximateHybridBounds() );
      } else if ( this._boundsMethod === 'fast' || this._boundsMethod === 'fastCanvas' ) {
        this.invalidateSelf( this.approximateSVGBounds() );
      } else {
        this.invalidateSelf( this.accurateCanvasBounds() );
      }
      
      // we may have changed renderers if parameters were changed!
      this.updateTextFlags();
    },
    
    // overrides from Strokable
    invalidateStroke: function() {
      // stroke can change both the bounds and renderer
      this.invalidateText();
    },
    
    // overrides from Fillable
    invalidateFill: function() {
      // fill type can change the renderer (gradient/fill not supported by DOM)
      this.invalidateText();
    },
    
    /*---------------------------------------------------------------------------*
    * Canvas support
    *----------------------------------------------------------------------------*/
    
    paintCanvas: function( wrapper ) {
      var context = wrapper.context;
      
      // extra parameters we need to set, but should avoid setting if we aren't drawing anything
      if ( this.hasFill() || this.hasStroke() ) {
        wrapper.setFont( this._font.getFont() );
        wrapper.setDirection( this._direction );
      }
      
      if ( this.hasFill() ) {
        this.beforeCanvasFill( wrapper ); // defined in Fillable
        context.fillText( this._text, 0, 0 );
        this.afterCanvasFill( wrapper ); // defined in Fillable
      }
      if ( this.hasStroke() ) {
        this.beforeCanvasStroke( wrapper ); // defined in Strokable
        context.strokeText( this._text, 0, 0 );
        this.afterCanvasStroke( wrapper ); // defined in Strokable
      }
    },
    
    /*---------------------------------------------------------------------------*
    * WebGL support
    *----------------------------------------------------------------------------*/
    
    paintWebGL: function( state ) {
      throw new Error( 'Text.prototype.paintWebGL unimplemented' );
    },
    
    /*---------------------------------------------------------------------------*
    * SVG support
    *----------------------------------------------------------------------------*/
    
    createSVGFragment: function( svg, defs, group ) {
      var element = document.createElementNS( 'http://www.w3.org/2000/svg', 'text' );
      element.appendChild( document.createTextNode( '' ) );
      return element;
    },
    
    updateSVGFragment: function( element ) {
      var isRTL = this._direction === 'rtl';
      
      // update the text-node's value
      element.lastChild.nodeValue = this._text;
      
      element.setAttribute( 'style', this.getSVGFillStyle() + this.getSVGStrokeStyle() );
      
      // element.setAttribute( 'text-anchor', 'start' ); // not needed right now (default is inherit)
      element.setAttribute( 'dominant-baseline', 'alphabetic' ); // to match Canvas right now
      element.setAttribute( 'direction', this._direction );
      
      // set all of the font attributes, since we can't use the combined one
      element.setAttribute( 'font-family', this._font.getFamily() );
      element.setAttribute( 'font-size', this._font.getSize() );
      element.setAttribute( 'font-style', this._font.getStyle() );
      element.setAttribute( 'font-weight', this._font.getWeight() );
      element.setAttribute( 'font-stretch', this._font.getStretch() );
    },
    
    // support patterns, gradients, and anything else we need to put in the <defs> block
    updateSVGDefs: function( svg, defs ) {
      // remove old definitions if they exist
      this.removeSVGDefs( svg, defs );
      
      // add new ones if applicable
      this.addSVGFillDef( svg, defs );
      this.addSVGStrokeDef( svg, defs );
    },
    
    // cleans up references created with udpateSVGDefs()
    removeSVGDefs: function( svg, defs ) {
      this.removeSVGFillDef( svg, defs );
      this.removeSVGStrokeDef( svg, defs );
    },
    
    /*---------------------------------------------------------------------------*
    * DOM support
    *----------------------------------------------------------------------------*/
    
    allowsMultipleDOMInstances: true,
    
    getDOMElement: function() {
      var div = document.createElement( 'div' );
      
      // so they are absolutely positioned compared to the containing DOM layer (that is positioned).
      // otherwise, two adjacent HTMLText elements will 'flow' and be positioned incorrectly
      div.style.position = 'absolute';
      return div;
    },
    
    updateDOMElement: function( div ) {
      var $div = $( div );
      div.style.font = this.getFont();
      div.style.color = this.getCSSFill();
      $div.width( this.getSelfBounds().width );
      $div.height( this.getSelfBounds().height );
      $div.empty(); // remove all children, including previously-created text nodes
      div.appendChild( this.getDOMTextNode() );
      div.setAttribute( 'dir', this._direction );
    },
    
    updateCSSTransform: function( transform, element ) {
      // since the DOM origin of the text is at the upper-left, and our Scenery origin is at the lower-left, we need to
      // shift the text vertically, postmultiplied with the entire transform.
      var yOffset = this.getSelfBounds().minY;
      var matrix = transform.getMatrix().timesMatrix( Matrix3.translation( 0, yOffset ) );
      scenery.Util.applyCSSTransform( matrix, element );
    },
    
    // a DOM node (not a Scenery DOM node, but an actual DOM node) with the text
    getDOMTextNode: function() {
      if ( this._isHTML ) {
        var span = document.createElement( 'span' );
        span.innerHTML = this.text;
        return span;
      } else {
        return document.createTextNode( this.text );
      }
    },
    
    /*---------------------------------------------------------------------------*
    * Bounds
    *----------------------------------------------------------------------------*/
    
    accurateCanvasBounds: function() {
      var node = this;
      var svgBounds = this.approximateSVGBounds(); // this seems to be slower than expected, mostly due to Font getters

      //If svgBounds are zero, then return the zero bounds
      if (svgBounds.width===0 && svgBounds.height===0){
        return svgBounds;
      }
      return scenery.Util.canvasAccurateBounds( function( context ) {
        context.font = node.font;
        context.direction = node.direction;
        context.fillText( node.text, 0, 0 );
      }, {
        precision: 0.5,
        resolution: 128,
        initialScale: 32 / Math.max( Math.abs( svgBounds.minX ), Math.abs( svgBounds.minY ), Math.abs( svgBounds.maxX ), Math.abs( svgBounds.maxY ) )
      } );
    },
    
    approximateCanvasWidth: function() {
      var context = scenery.scratchContext;
      context.font = this.font;
      context.direction = this.direction;
      return context.measureText( this.text ).width;
    },
    
    approximateSVGBounds: function() {
      if ( !svgTextSizeContainer.parentNode ) {
        if ( document.body ) {
          document.body.appendChild( svgTextSizeContainer );
        } else {
          // TODO: better way to handle the hybridTextNode being added inside the HEAD? Requiring a body for proper operation might be a problem.
          if ( initializingHybridTextNode ) {
            // if this is almost assuredly the hybridTextNode, return nothing for now. TODO: better way of handling this! it's a hack!
            return Bounds2.NOTHING;
          } else {
            throw new Error( 'No document.body and trying to get approximate SVG bounds of a Text node' );
          }
        }
      }
      this.updateSVGFragment( svgTextSizeElement );
      var rect = svgTextSizeElement.getBBox();
      return new Bounds2( rect.x, rect.y, rect.x + rect.width, rect.y + rect.height );
    },
    
    approximateHybridBounds: function() {
      if ( !hybridTextNode ) {
        return Bounds2.NOTHING; // we are the hybridTextNode, ignore us
      }
      
      if ( this._font._cachedSVGBounds === undefined ) {
        hybridTextNode.setFont( this._font );
        this._font._cachedSVGBounds = hybridTextNode.getBounds();
      }
      
      var canvasWidth = this.approximateCanvasWidth();
      var verticalBounds = this._font._cachedSVGBounds;
      
      // it seems that SVG bounds generally have x=0, so we hard code that here
      return new Bounds2( 0, verticalBounds.minY, canvasWidth, verticalBounds.maxY );
    },
    
    approximateDOMBounds: function() {
      var maxHeight = 1024; // technically this will fail if the font is taller than this!
      var isRTL = this.direction === 'rtl';
      
      // <div style="position: absolute; left: 0; top: 0; padding: 0 !important; margin: 0 !important;"><span id="baselineSpan" style="font-family: Verdana; font-size: 25px;">QuipTaQiy</span><div style="vertical-align: baseline; display: inline-block; width: 0; height: 500px; margin: 0 important!; padding: 0 important!;"></div></div>
      
      var div = document.createElement( 'div' );
      $( div ).css( {
        position: 'absolute',
        left: 0,
        top: 0,
        padding: '0 !important',
        margin: '0 !important',
        display: 'hidden'
      } );
      
      var span = document.createElement( 'span' );
      $( span ).css( 'font', this.getFont() );
      span.appendChild( this.getDOMTextNode() );
      span.setAttribute( 'direction', this._direction );
      
      var fakeImage = document.createElement( 'div' );
      $( fakeImage ).css( {
        'vertical-align': 'baseline',
        display: 'inline-block',
        width: 0,
        height: maxHeight + 'px',
        margin: '0 !important',
        padding: '0 !important'
      } );
      
      div.appendChild( span );
      div.appendChild( fakeImage );
      
      document.body.appendChild( div );
      var rect = span.getBoundingClientRect();
      var divRect = div.getBoundingClientRect();
      // add 1 pixel to rect.right to prevent HTML text wrapping
      var result = new Bounds2( rect.left, rect.top - maxHeight, rect.right + 1, rect.bottom - maxHeight ).shifted( -divRect.left, -divRect.top );
      // console.log( 'result: ' + result );
      document.body.removeChild( div );
      
      var width = rect.right - rect.left;
      return result.shiftedX( isRTL ? -width : 0 ); // should we even swap here?
    },
    
    /*---------------------------------------------------------------------------*
    * Self setters / getters
    *----------------------------------------------------------------------------*/
    
    setFont: function( font ) {
      if ( this.font !== font ) {
        this._font = font instanceof scenery.Font ? font : new scenery.Font( font );
        this.invalidateText();
      }
      return this;
    },
    
    // NOTE: returns mutable copy for now, consider either immutable version, defensive copy, or note about invalidateText()
    getFont: function() {
      return this._font.getFont();
    },
    
    setDirection: function( direction ) {
      this._direction = direction;
      this.invalidateText();
      return this;
    },
    
    getDirection: function() {
      return this._direction;
    },
    
    isPainted: function() {
      return true;
    },
    
    getBasicConstructor: function( propLines ) {
      return 'new scenery.Text( \'' + escapeHTML( this._text.replace( /'/g, '\\\'' ) ) + '\', {' + propLines + '} )';
    },
    
    getPropString: function( spaces, includeChildren ) {
      var result = Node.prototype.getPropString.call( this, spaces, includeChildren );
      result = this.appendFillablePropString( spaces, result );
      result = this.appendStrokablePropString( spaces, result );
      
      // TODO: if created again, deduplicate with Node's getPropString
      function addProp( key, value, nowrap ) {
        if ( result ) {
          result += ',\n';
        }
        if ( !nowrap && typeof value === 'string' ) {
          result += spaces + key + ': \'' + value + '\'';
        } else {
          result += spaces + key + ': ' + value;
        }
      }
      
      if ( this.font !== new scenery.Font().getFont() ) {
        addProp( 'font', this.font.replace( /'/g, '\\\'' ) );
      }
      
      if ( this._direction !== 'ltr' ) {
        addProp( 'direction', this._direction );
      }
      
      return result;
    }
  } );
  
  /*---------------------------------------------------------------------------*
  * Font setters / getters
  *----------------------------------------------------------------------------*/
  
  function addFontForwarding( propertyName, fullCapitalized, shortUncapitalized ) {
    var getterName = 'get' + fullCapitalized;
    var setterName = 'set' + fullCapitalized;
    
    Text.prototype[getterName] = function() {
      // use the ES5 getter to retrieve the property. probably somewhat slow.
      return this._font[ shortUncapitalized ];
    };
    
    Text.prototype[setterName] = function( value ) {
      // create a full copy of our font instance
      var ob = {};
      ob[shortUncapitalized] = value;
      var newFont = this._font.copy( ob );
      
      // apply the new Font. this should call invalidateText() as normal
      this.setFont( newFont );
      return this;
    };
    
    Object.defineProperty( Text.prototype, propertyName, { set: Text.prototype[setterName], get: Text.prototype[getterName] } );
  }
  
  addFontForwarding( 'fontWeight', 'FontWeight', 'weight' );
  addFontForwarding( 'fontFamily', 'FontFamily', 'family' );
  addFontForwarding( 'fontStretch', 'FontStretch', 'stretch' );
  addFontForwarding( 'fontStyle', 'FontStyle', 'style' );
  addFontForwarding( 'fontSize', 'FontSize', 'size' );
  addFontForwarding( 'lineHeight', 'LineHeight', 'lineHeight' );
  
  Text.prototype._mutatorKeys = [ 'boundsMethod', 'text', 'font', 'fontWeight', 'fontFamily', 'fontStretch', 'fontStyle', 'fontSize', 'lineHeight',
                                  'direction' ].concat( Node.prototype._mutatorKeys );
  
  Text.prototype._supportedRenderers = [ Renderer.Canvas, Renderer.SVG, Renderer.DOM ];
  Text.prototype._supportedRenderersWithFastBounds = [ Renderer.SVG, Renderer.DOM ]; // renderers for fast (SVG/DOM) bounds, since canvas dirty regions would present issues
  
  // font-specific ES5 setters and getters are defined using addFontForwarding above
  Object.defineProperty( Text.prototype, 'font', { set: Text.prototype.setFont, get: Text.prototype.getFont } );
  Object.defineProperty( Text.prototype, 'text', { set: Text.prototype.setText, get: Text.prototype.getText } );
  Object.defineProperty( Text.prototype, 'direction', { set: Text.prototype.setDirection, get: Text.prototype.getDirection } );
  Object.defineProperty( Text.prototype, 'boundsMethod', { set: Text.prototype.setBoundsMethod, get: Text.prototype.getBoundsMethod } );
  
  // mix in support for fills and strokes
  /* jshint -W064 */
  Fillable( Text );
  Strokable( Text );
  
  initializingHybridTextNode = true;
  hybridTextNode = new Text( 'm', { boundsMethod: 'fast' } );
  initializingHybridTextNode = false;

  return Text;
} );



// Copyright 2002-2013, University of Colorado

/**
 * Images
 *
 * TODO: setImage / getImage and the whole toolchain that uses that
 * TODO: allow multiple DOM instances (create new HTMLImageElement elements)
 * TODO: SVG support
 * TODO: support rendering a Canvas to DOM (single instance)
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/Image',['require','PHET_CORE/inherit','DOT/Bounds2','SCENERY/scenery','SCENERY/nodes/Node','SCENERY/layers/Renderer','SCENERY/util/Util','SCENERY/util/Util'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Bounds2 = require( 'DOT/Bounds2' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  var Node = require( 'SCENERY/nodes/Node' ); // Image inherits from Node
  var Renderer = require( 'SCENERY/layers/Renderer' ); // we need to specify the Renderer in the prototype
  var objectCreate = require( 'SCENERY/util/Util' ).objectCreate;
  require( 'SCENERY/util/Util' );
  
  /*
   * Canvas renderer supports the following as 'image':
   *     URL (string)             // works, but does NOT support bounds-based parameter object keys like 'left', 'centerX', etc.
   *                              // also necessary to force updateScene() after it has loaded
   *     HTMLImageElement         // works
   *     HTMLVideoElement         // not tested
   *     HTMLCanvasElement        // works, and forces the canvas renderer
   *     CanvasRenderingContext2D // not tested, but bad luck in past
   *     ImageBitmap              // good luck creating this. currently API for window.createImageBitmap not implemented
   * SVG renderer supports the following as 'image':
   *     URL (string)
   *     HTMLImageElement
   */
  scenery.Image = function Image( image, options ) {
    sceneryAssert && sceneryAssert( image, "image should be available" );
    
    // allow not passing an options object
    options = options || {};
    
    // rely on the setImage call from the super constructor to do the setup
    if ( image ) {
      options.image = image;
    }
    
    var self = this;
    // allows us to invalidate our bounds whenever an image is loaded
    this.loadListener = function( event ) {
      self.invalidateImage();
      
      // don't leak memory!
      self._image.removeEventListener( 'load', self.loadListener );
    };
    
    Node.call( this, options );
  };
  var Image = scenery.Image;
  
  inherit( Node, Image, {
    allowsMultipleDOMInstances: false, // TODO: support multiple instances
    
    invalidateImage: function() {
      this.invalidateSelf( new Bounds2( 0, 0, this.getImageWidth(), this.getImageHeight() ) );
    },
    
    getImage: function() {
      return this._image;
    },
    
    setImage: function( image ) {
      var self = this;
      
      if ( this._image !== image && ( typeof image !== 'string' || !this._image || image !== this._image.src ) ) {
        // don't leak memory by referencing old images
        if ( this._image ) {
          this._image.removeEventListener( 'load', this.loadListener );
        }
        
        if ( typeof image === 'string' ) {
          // create an image with the assumed URL
          var src = image;
          image = document.createElement( 'img' );
          image.addEventListener( 'load', this.loadListener );
          image.src = src;
        } else if ( image instanceof HTMLImageElement ) {
          // only add a listener if we probably haven't loaded yet
          if ( !image.width || !image.height ) {
            image.addEventListener( 'load', this.loadListener );
          }
        }
        
        // swap supported renderers if necessary TODO: share this code dealing with compatible renderer changes
        if ( image instanceof HTMLCanvasElement ) {
          if ( !this.hasOwnProperty( '_supportedRenderers' ) ) {
            this._supportedRenderers = [ Renderer.Canvas ];
            this.markLayerRefreshNeeded();
          }
        } else {
          if ( this.hasOwnProperty( '_supportedRenderers' ) ) {
            delete this._supportedRenderers; // will leave prototype intact
            this.markLayerRefreshNeeded();
          }
        }
        
        this._image = image;
        this.invalidateImage(); // yes, if we aren't loaded yet this will give us 0x0 bounds
      }
      return this;
    },
    
    getImageWidth: function() {
      return this._image.width;
    },
    
    getImageHeight: function() {
      return this._image.height;
    },
    
    getImageURL: function() {
      return this._image.src;
    },
    
    // signal that we are actually rendering something
    isPainted: function() {
      return true;
    },
    
    /*---------------------------------------------------------------------------*
    * Canvas support
    *----------------------------------------------------------------------------*/
    
    // TODO: add SVG / DOM support
    paintCanvas: function( wrapper ) {
      wrapper.context.drawImage( this._image, 0, 0 );
    },
    
    /*---------------------------------------------------------------------------*
    * WebGL support
    *----------------------------------------------------------------------------*/
    
    paintWebGL: function( state ) {
      throw new Error( 'paintWebGL:nimplemented' );
    },
    
    /*---------------------------------------------------------------------------*
    * SVG support
    *----------------------------------------------------------------------------*/
    
    createSVGFragment: function( svg, defs, group ) {
      var element = document.createElementNS( 'http://www.w3.org/2000/svg', 'image' );
      element.setAttribute( 'x', 0 );
      element.setAttribute( 'y', 0 );
      return element;
    },
    
    updateSVGFragment: function( element ) {
      // like <image xlink:href='http://phet.colorado.edu/images/phet-logo-yellow.png' x='0' y='0' height='127px' width='242px'/>
      var xlinkns = 'http://www.w3.org/1999/xlink';
      
      element.setAttribute( 'width', this.getImageWidth() + 'px' );
      element.setAttribute( 'height', this.getImageHeight() + 'px' );
      element.setAttributeNS( xlinkns, 'xlink:href', this.getImageURL() );
    },
    
    /*---------------------------------------------------------------------------*
    * DOM support
    *----------------------------------------------------------------------------*/
    
    getDOMElement: function() {
      this._image.style.display = 'block';
      this._image.style.position = 'absolute';
      this._image.style.left = '0';
      this._image.style.top = '0';
      return this._image;
    },
    
    updateDOMElement: function( image ) {
      if ( image.src !== this._image.src ) {
        image.src = this._image.src;
      }
    },
    
    updateCSSTransform: function( transform, element ) {
      // TODO: extract this out, it's completely shared!
      scenery.Util.applyCSSTransform( transform.getMatrix(), element );
    },
    
    set image( value ) { this.setImage( value ); },
    get image() { return this.getImage(); },
    
    getBasicConstructor: function( propLines ) {
      return 'new scenery.Image( \'' + ( this._image.src ? this._image.src.replace( /'/g, '\\\'' ) : 'other' ) + '\', {' + propLines + '} )';
    }
  } );
  
  Image.prototype._mutatorKeys = [ 'image' ].concat( Node.prototype._mutatorKeys );
  
  Image.prototype._supportedRenderers = [ Renderer.Canvas, Renderer.SVG, Renderer.DOM ];
  
  // utility for others
  Image.createSVGImage = function( url, width, height ) {
    var xlinkns = 'http://www.w3.org/1999/xlink';
    var svgns = 'http://www.w3.org/2000/svg';
    
    var element = document.createElementNS( svgns, 'image' );
    element.setAttribute( 'x', 0 );
    element.setAttribute( 'y', 0 );
    element.setAttribute( 'width', width + 'px' );
    element.setAttribute( 'height', height + 'px' );
    element.setAttributeNS( xlinkns, 'xlink:href', url );
    
    return element;
  };
  
  return Image;
} );



// Copyright 2002-2013, University of Colorado

/**
 * DOM nodes. Currently lightweight handling
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/DOM',['require','PHET_CORE/inherit','PHET_CORE/escapeHTML','DOT/Bounds2','SCENERY/scenery','SCENERY/nodes/Node','SCENERY/layers/Renderer','SCENERY/util/Util','SCENERY/util/Util'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var escapeHTML = require( 'PHET_CORE/escapeHTML' );
  var Bounds2 = require( 'DOT/Bounds2' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  var Node = require( 'SCENERY/nodes/Node' ); // DOM inherits from Node
  var Renderer = require( 'SCENERY/layers/Renderer' );
  var objectCreate = require( 'SCENERY/util/Util' ).objectCreate;
  require( 'SCENERY/util/Util' );
  
  scenery.DOM = function DOM( element, options ) {
    options = options || {};
    
    this._interactive = false;
    
    // unwrap from jQuery if that is passed in, for consistency
    if ( element && element.jquery ) {
      element = element[0];
    }
    
    this._container = document.createElement( 'div' );
    this._$container = $( this._container );
    this._$container.css( 'position', 'absolute' );
    this._$container.css( 'left', 0 );
    this._$container.css( 'top', 0 );
    
    this.invalidateDOMLock = false;
    
    // so that the mutator will call setElement()
    options.element = element;
    
    // will set the element after initializing
    Node.call( this, options );
  };
  var DOM = scenery.DOM;
  
  inherit( Node, DOM, {
    // we use a single DOM instance, so this flag should indicate that we don't support duplicating it
    allowsMultipleDOMInstances: false,
    
    // needs to be attached to the DOM tree for this to work
    calculateDOMBounds: function() {
      // var boundingRect = this._element.getBoundingClientRect();
      // return new Bounds2( 0, 0, boundingRect.width, boundingRect.height );
      var $element = $( this._element );
      return new Bounds2( 0, 0, $element.width(), $element.height() );
    },
    
    createTemporaryContainer: function() {
      var temporaryContainer = document.createElement( 'div' );
      $( temporaryContainer ).css( {
        display: 'hidden',
        padding: '0 !important',
        margin: '0 !important',
        position: 'absolute',
        left: 0,
        top: 0,
        width: 65535,
        height: 65535
      } );
      return temporaryContainer;
    },
    
    invalidateDOM: function() {
      // prevent this from being executed as a side-effect from inside one of its own calls
      if ( this.invalidateDOMLock ) {
        return;
      }
      this.invalidateDOMLock = true;
      
      // we will place ourselves in a temporary container to get our real desired bounds
      var temporaryContainer = this.createTemporaryContainer();
      
      // move to the temporary container
      this._container.removeChild( this._element );
      temporaryContainer.appendChild( this._element );
      document.body.appendChild( temporaryContainer );
      
      // bounds computation and resize our container to fit precisely
      var selfBounds = this.calculateDOMBounds();
      this.invalidateSelf( selfBounds );
      this._$container.width( selfBounds.getWidth() );
      this._$container.height( selfBounds.getHeight() );
      
      // move back to the main container
      document.body.removeChild( temporaryContainer );
      temporaryContainer.removeChild( this._element );
      this._container.appendChild( this._element );
      
      this.invalidateDOMLock = false;
    },
    
    getDOMElement: function() {
      return this._container;
    },
    
    updateDOMElement: function( container ) {
      // nothing needed, since we are just displaying a single DOM element
    },
    
    updateCSSTransform: function( transform, element ) {
      // faster to use our jQuery reference instead of wrapping element
      scenery.Util.applyCSSTransform( transform.getMatrix(), this._container );
    },
    
    isPainted: function() {
      return true;
    },
    
    setElement: function( element ) {
      if ( this._element !== element ) {
        if ( this._element ) {
          this._container.removeChild( this._element );
        }
        
        this._element = element;
        this._$element = $( element );
        
        this._container.appendChild( this._element );
        
        // TODO: bounds issue, since this will probably set to empty bounds and thus a repaint may not draw over it
        this.invalidateDOM();  
      }

      return this; // allow chaining
    },
    
    getElement: function() {
      return this._element;
    },
    
    setInteractive: function( interactive ) {
      if ( this._interactive !== interactive ) {
        this._interactive = interactive;
        
        // TODO: anything needed here?
      }
    },
    
    isInteractive: function() {
      return this._interactive;
    },
    
    set element( value ) { this.setElement( value ); },
    get element() { return this.getElement(); },
    
    set interactive( value ) { this.setInteractive( value ); },
    get interactive() { return this.isInteractive(); },
    
    getBasicConstructor: function( propLines ) {
      return 'new scenery.DOM( $( \'' + escapeHTML( this._container.innerHTML.replace( /'/g, '\\\'' ) ) + '\' ), {' + propLines + '} )';
    },
    
    getPropString: function( spaces, includeChildren ) {
      var result = Node.prototype.getPropString.call( this, spaces, includeChildren );
      if ( this.interactive ) {
        if ( result ) {
          result += ',\n';
        }
        result += spaces + 'interactive: true';
      }
      return result;
    }
  } );
  
  DOM.prototype._mutatorKeys = [ 'element', 'interactive' ].concat( Node.prototype._mutatorKeys );
  
  DOM.prototype._supportedRenderers = [ Renderer.DOM ];
  
  return DOM;
} );



// Copyright 2002-2013, University of Colorado Boulder

/**
 * Creates an array of results from an iterator that takes a callback.
 *
 * For instance, if calling a function f( g ) will call g( 1 ), g( 2 ), and g( 3 ),
 * collect( function( callback ) { f( callback ); } );
 * will return [1,2,3].
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('PHET_CORE/collect',['require'], function( require ) {
  
  
  return function collect( iterate ) {
    var result = [];
    iterate( function( ob ) {
      result.push( ob );
    } );
    return result;
  };
} );

// Copyright 2002-2013, University of Colorado

/**
 * Accessibility peer, which is added to the dom for focus and keyboard navigation.
 *
 * @author Sam Reid
 */

define('SCENERY/util/AccessibilityPeer',['require','PHET_CORE/inherit','SCENERY/scenery'], function( require ) {
  

  var inherit = require( 'PHET_CORE/inherit' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  var AccessibilityPeer = scenery.AccessibilityPeer = function AccessibilityPeer( instance, element, options ) {
    var peer = this;
    
    options = options || {};

    //Defaulting to 0 would mean using the document order, which can easily be incorrect for a PhET simulation.
    //For any of the nodes to use a nonzero tabindex, they must all use a nonzero tabindex, see #40
    options.tabIndex = options.tabIndex || 1;
    
    // TODO: if element is a DOM element, verify that no other accessibility peer is using it! (add a flag, and remove on disposal)
    this.element = ( typeof element === 'string' ) ? $( element )[0] : element;

    if ( options.label ) {
      this.peerElement = document.createElement( 'div' );
      this.element.id = 'peer-' + instance.trail.getUniqueId();
      var label = document.createElement('label');
      label.appendChild(document.createTextNode(options.label));
      label.setAttribute('for',this.element.id);
      this.peerElement.appendChild(label);
      this.peerElement.appendChild(this.element);
    } else{
      this.peerElement = this.element;
    }

    this.instance = instance;
    this.trail = instance.trail;
    
    this.element.setAttribute( 'tabindex', options.tabIndex );
    this.element.style.position = 'absolute';
    
    // TODO: batch these also if the Scene is batching events
    var scene = instance.getScene();
    this.clickListener = function PeerClickListener( event ) {
      sceneryAccessibilityLog && sceneryAccessibilityLog( 'peer click on ' + instance.toString() + ': ' + instance.getNode().constructor.name );
      if ( options.click ) { options.click( event ); }
    };
    this.focusListener = function PeerFocusListener( event ) {
      sceneryAccessibilityLog && sceneryAccessibilityLog( 'peer focused: ' + instance.toString() + ': ' + instance.getNode().constructor.name );
      scene.focusPeer( peer );
    };
    this.blurListener = function PeerBlurListener( event ) {
      sceneryAccessibilityLog && sceneryAccessibilityLog( 'peer blurred: ' + instance.toString() + ': ' + instance.getNode().constructor.name );
      scene.blurPeer( peer );
    };
    this.element.addEventListener( 'click', this.clickListener );
    this.element.addEventListener( 'focus', this.focusListener );
    this.element.addEventListener( 'blur', this.blurListener );

    this.keepPeerBoundsInSync = true;
    if ( this.keepPeerBoundsInSync ) {
      this.boundsSyncListener = this.syncBounds.bind( this );

      instance.getNode().addEventListener( 'bounds', this.boundsSyncListener );
      this.syncBounds();

      //When the scene resizes, update the peer bounds
      instance.getScene().addEventListener( 'resize', this.boundsSyncListener );

      //Initial layout
      window.setTimeout( this.syncBounds.bind( this ), 30 );
    }
  };

  AccessibilityPeer.prototype = {
    constructor: AccessibilityPeer,
    
    dispose: function() {
      this.element.removeEventListener( 'click', this.clickListener );
      this.element.removeEventListener( 'focus', this.focusListener );
      this.element.removeEventListener( 'blur', this.blurListener );
      
      // don't leak memory
      if ( this.keepPeerBoundsInSync ) {
        this.instance.getNode().removeEventListener( 'bounds', this.boundsSyncListener );
        this.instance.getScene().removeEventListener( 'resize', this.boundsSyncListener );
      }
    },
    
    getGlobalBounds: function() {
      return this.trail.parentToGlobalBounds( this.trail.lastNode().getBounds() ).roundedOut();
    },
    
    syncBounds: function() {
      var globalBounds = this.getGlobalBounds();
      this.element.style.left = globalBounds.x + 'px';
      this.element.style.top = globalBounds.y + 'px';
      this.element.style.width = globalBounds.width + 'px';
      this.element.style.height = globalBounds.height + 'px';
    }
  };
  
  return AccessibilityPeer;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Live region is used with accessibility to read out changes in model state.
 * Should conform to the Axon property interface to make it easy to interchange.
 *
 * @author Sam Reid
 */

define('SCENERY/util/LiveRegion',['require','PHET_CORE/inherit','PHET_CORE/escapeHTML','SCENERY/scenery'], function( require ) {
  

  var inherit = require( 'PHET_CORE/inherit' );
  var escapeHTML = require( 'PHET_CORE/escapeHTML' );

  var scenery = require( 'SCENERY/scenery' );

  var LiveRegion = scenery.LiveRegion = function LiveRegion( instance, property, options ) {
    var liveRegion = this;
    this.property = property;
    options = options || {};

    //Defaulting to 0 would mean using the document order, which can easily be incorrect for a PhET simulation.
    //For any of the nodes to use a nonzero tabindex, they must all use a nonzero tabindex, see #40
    options.tabIndex = options.tabIndex || 1;

    // TODO: if element is a DOM element, verify that no other accessibility liveRegion is using it! (add a flag, and remove on disposal)
//    this.element = '<div role="region" id="bird-info" aria-live="polite">';
    this.element = document.createElement( 'div' );
    this.element.setAttribute( 'aria-live', 'polite' );
    this.element.setAttribute( 'role', 'region' );
    this.textNode = document.createTextNode( '' );
    this.element.appendChild( this.textNode );

    //Just setting the text causes NVDA to read deltas, you have to replace the node to have it read the text
    this.listener = function( newText ) {
      liveRegion.element.removeChild( liveRegion.textNode );
      liveRegion.textNode = document.createTextNode( newText );
      liveRegion.element.appendChild( liveRegion.textNode );
    };
    property.link( this.listener );
  };

  LiveRegion.prototype = {
    constructor: scenery.LiveRegion,
    dispose: function() { this.property.unlink( this.listener ); }
  };

  return LiveRegion;
} );
// Copyright 2002-2013, University of Colorado

/**
 * An Instance of a Node in the expanded tree form.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/Instance',['require','SCENERY/scenery','SCENERY/util/AccessibilityPeer','SCENERY/util/LiveRegion'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/util/AccessibilityPeer' );
  require( 'SCENERY/util/LiveRegion' );
  
  var accessibility = window.has && window.has( 'scenery.accessibility' );
  
  // layer should be null if the trail isn't to a painted node
  scenery.Instance = function Instance( trail, layer, parent ) {
    this.trail = trail; // trail may be assumed to be stale, for performance reasons
    this.layer = layer;
    this.oldLayer = layer; // used during stitching
    
    // assertion not enabled, since at the start we don't specify a layer (it will be constructed later)
    // sceneryAssert && sceneryAssert( trail.lastNode().isPainted() === ( layer !== null ), 'Has a layer iff is painted' );
    
    // TODO: SVG layer might want to put data (group/fragment) references here (indexed by layer ID)
    this.data = {};
    
    // TODO: ensure that we can track this? otherwise remove it for memory and speed
    this.parent = parent;
    this.children = [];
    
    this.peers = []; // list of AccessibilityPeer instances attached to this trail
    this.liveRegions = []; // list of LiveRegion instances attached to this trail
    
    // TODO: track these? should significantly accelerate subtree-changing operations
    this.startAffectedLayer = null;
    this.endAffectedLayer = null;
    
    trail.setImmutable(); // make sure our Trail doesn't change from under us
    
    if ( accessibility ) {
      this.addPeers();
      this.addLiveRegions();
    }
    
    phetAllocation && phetAllocation( 'Instance' );
  };
  var Instance = scenery.Instance;
  
  Instance.prototype = {
    constructor: Instance,
    
    getScene: function() {
      return this.trail.rootNode();
    },
    get scene() { return this.getScene(); },
    
    getNode: function() {
      return this.trail.lastNode();
    },
    get node() { return this.getNode(); },
    
    changeLayer: function( newLayer ) {
      // NOTE: DO NOT change _layerTrails somehow in this function. we don't make a defensive copy of that array during stitching!
      if ( newLayer !== this.layer ) {
        sceneryLayerLog && sceneryLayerLog( 'changing instance ' + this.trail.toString() + ' to layer ' + ( newLayer ? '#' + newLayer.id : 'null' ) );
        this.layer && ( this.layer._instanceCount -= 1 );
        this.layer = newLayer;
        this.layer && ( this.layer._instanceCount += 1 );
      }
    },
    
    updateLayer: function() {
      if ( this.layer !== this.oldLayer ) {
        // we may have stale indices
        this.reindex();
        
        if ( sceneryLayerLog ) {
          if ( this.oldLayer && this.layer ) {
            sceneryLayerLog( 'moving instance ' + this.trail.toString() + ' from layer #' + this.oldLayer.id + ' to layer #' + this.layer.id );
          } else if ( this.layer ) {
            sceneryLayerLog( 'adding instance ' + this.trail.toString() + ' to layer #' + this.layer.id );
          } else {
            sceneryLayerLog( 'remove instance ' + this.trail.toString() + ' from layer #' + this.oldLayer.id );
          }
        }
        if ( this.oldLayer ) {
          this.oldLayer.removeInstance( this );
        }
        if ( this.layer ) {
          this.layer.addInstance( this );
        }
        this.oldLayer = this.layer;
      }
    },
    
    createChild: function( childNode, index ) {
      var childTrail = this.trail.copy().addDescendant( childNode );
      var childInstance = new scenery.Instance( childTrail, null, this );
      sceneryLayerLog && sceneryLayerLog( 'Instance.createChild: ' + childInstance.toString() );
      this.insertInstance( index, childInstance );
      childInstance.getNode().addInstance( childInstance );
      
      return childInstance;
    },
    
    addInstance: function( instance ) {
      sceneryAssert && sceneryAssert( instance, 'Instance.addInstance cannot have falsy parameter' );
      this.children.push( instance );
    },
    
    insertInstance: function( index, instance ) {
      sceneryAssert && sceneryAssert( instance, 'Instance.insert cannot have falsy instance parameter' );
      sceneryAssert && sceneryAssert( index >= 0 && index <= this.children.length, 'Instance.insert has bad index ' + index + ' for length ' + this.children.length );
      this.children.splice( index, 0, instance );
    },
    
    removeInstance: function( index ) {
      sceneryAssert && sceneryAssert( typeof index === 'number' );
      this.children.splice( index, 1 );
    },
    
    reindex: function() {
      this.trail.reindex();
    },
    
    // TODO: rename, so that it indicates that it removes the instance from the node
    dispose: function() {
      if ( this.layer ) {
        this.changeLayer( null );
        this.updateLayer();
      }
      this.parent = null;
      this.children.length = 0;
      this.getNode().removeInstance( this );
      
      if ( accessibility ) {
        this.removePeers();
        this.removeLiveRegions();
      }
    },
    
    equals: function( other ) {
      sceneryAssert && sceneryAssert( ( this === other ) === this.trail.equals( other.trail ), 'We assume a 1-1 mapping from trails to instances' );
      return this === other;
    },
    
    // standard -1,0,1 comparison with another instance, as a total ordering from the render order
    compare: function( other ) {
      return this.trail.compare( other.trail );
    },
    
    getLayerString: function() {
      return this.layer ? ( this.layer.getName() + '#' + this.layer.getId() ) : '-';
    },
    
    getTrailString: function() {
      return this.trail.toString();
    },
    
    toString: function() {
      return '{' + this.getTrailString() + ', ' + this.getLayerString() + '}';
    },
    
    getAffectedLayers: function() {
      // TODO: optimize this using pre-recorded versions?
      this.reindex();
      return this.getScene().affectedLayers( this.trail );
    },
    
    addPeers: function() {
      var node = this.getNode();
      var scene = this.getScene();
      
      var len = node._peers.length;
      if ( len ) {
        for ( var i = 0; i < len; i++ ) {
          var desc = node._peers[i];
          var peer = new scenery.AccessibilityPeer( this, desc.element, desc.options );
          scene.addPeer( peer );
          this.peers.push( peer );
        }
      }
    },
    
    removePeers: function() {
      var scene = this.getScene();
      
      var i = this.peers.length;
      while ( i-- ) {
        var peer = this.peers[i];
        scene.removePeer( peer );
        peer.dispose();
      }
      
      this.peers.length = 0; // clear this.peers
    },

    addLiveRegions: function() {
      var thisInstance = this;
      var node = this.getNode();
      var scene = this.getScene();

      if ( node._liveRegions.length ) {
        _.each( node._liveRegions, function( item ) {
          var liveRegion = new scenery.LiveRegion( thisInstance, item.property, item.options );
          scene.addLiveRegion( liveRegion );
          thisInstance.liveRegions.push( liveRegion );
        } );
      }
    },

    removeLiveRegions: function() {
      var scene = this.getScene();

      _.each( this.liveRegions, function( liveRegion ) {
        scene.removeLiveRegion( liveRegion );
        liveRegion.dispose();
      } );
      
      this.peers.length = 0; // clear this.peers
    },
    
    /*---------------------------------------------------------------------------*
    * Events from the Node
    *----------------------------------------------------------------------------*/
    
    notifyVisibilityChange: function() {
      sceneryEventLog && sceneryEventLog( 'notifyVisibilityChange: ' + this.trail.toString() + ', ' + this.getLayerString() );
      
      var affectedLayers = this.getAffectedLayers();
      var i = affectedLayers.length;
      while ( i-- ) {
        affectedLayers[i].notifyVisibilityChange( this );
      }
    },
    
    notifyOpacityChange: function() {
      sceneryEventLog && sceneryEventLog( 'notifyOpacityChange: ' + this.trail.toString() + ', ' + this.getLayerString() );
      
      var affectedLayers = this.getAffectedLayers();
      var i = affectedLayers.length;
      while ( i-- ) {
        affectedLayers[i].notifyOpacityChange( this );
      }
    },
    
    notifyBeforeSelfChange: function() {
      sceneryEventLog && sceneryEventLog( 'notifyBeforeSelfChange: ' + this.trail.toString() + ', ' + this.getLayerString() );
      // TODO: Canvas will only need to be notified of these once in-between scene updates
      // TODO: walk up the "tree" to see if any ancestors did this (in which case we don't need to)
      // e.g. this.oldPaint = true, etc.
      this.layer.notifyBeforeSelfChange( this );
    },
    
    notifyBeforeSubtreeChange: function() {
      sceneryEventLog && sceneryEventLog( 'notifyBeforeSubtreeChange: ' + this.trail.toString() + ', ' + this.getLayerString() );
      
      var affectedLayers = this.getAffectedLayers();
      var i = affectedLayers.length;
      while ( i-- ) {
        affectedLayers[i].notifyBeforeSubtreeChange( this );
      }
    },
    
    notifyDirtySelfPaint: function() {
      sceneryEventLog && sceneryEventLog( 'notifyDirtySelfPaint: ' + this.trail.toString() + ', ' + this.getLayerString() );
      sceneryAssert && sceneryAssert( this.getNode().isPainted(), 'Instance needs to be painted for notifyDirtySelfPaint' );
      this.layer.notifyDirtySelfPaint( this );
    },
    
    // TODO: consider special post-transform type?
    notifyDirtySubtreePaint: function() {
      sceneryEventLog && sceneryEventLog( 'notifyDirtySubtreePaint: ' + this.trail.toString() + ', ' + this.getLayerString() );
      
      var affectedLayers = this.getAffectedLayers();
      var i = affectedLayers.length;
      while ( i-- ) {
        affectedLayers[i].notifyDirtySubtreePaint( this );
      }
    },
    
    notifyDirtySubtreeBounds: function() {
      sceneryEventLog && sceneryEventLog( 'notifyDirtySubtreeBounds: ' + this.trail.toString() + ', ' + this.getLayerString() );
      
      var affectedLayers = this.getAffectedLayers();
      var i = affectedLayers.length;
      while ( i-- ) {
        affectedLayers[i].notifyDirtySubtreeBounds( this );
      }
    },
    
    notifyTransformChange: function() {
      sceneryEventLog && sceneryEventLog( 'notifyTransformChange: ' + this.trail.toString() + ', ' + this.getLayerString() );
      
      var affectedLayers = this.getAffectedLayers();
      var i = affectedLayers.length;
      while ( i-- ) {
        affectedLayers[i].notifyTransformChange( this );
      }
    },
    
    notifyBoundsAccuracyChange: function() {
      sceneryEventLog && sceneryEventLog( 'notifyBoundsAccuracyChange: ' + this.trail.toString() + ', ' + this.getLayerString() );
      this.layer.notifyBoundsAccuracyChange( this );
    },
    
    notifyStitch: function( match ) {
      sceneryEventLog && sceneryEventLog( 'notifyStitch: ' + this.trail.toString() + ' match:' + match + ', ' + this.getLayerString() );
      this.getScene().stitch( match );
    },
    
    markForLayerRefresh: function() {
      sceneryEventLog && sceneryEventLog( 'markForLayerRefresh: ' + this.trail.toString() + ', ' + this.getLayerString() );
      this.getScene().markSceneForLayerRefresh( this );
    },
    
    markForInsertion: function( child, index ) {
      sceneryEventLog && sceneryEventLog( 'markForInsertion: ' + this.trail.toString() + ' child:' + child.id + ', index: ' + index + ', ' + this.getLayerString() );
      
      this.reindex();
      this.getScene().markSceneForInsertion( this, child, index );
    },
    
    markForRemoval: function( child, index ) {
      sceneryEventLog && sceneryEventLog( 'markForRemoval: ' + this.trail.toString() + ' child:' + child.id + ', index: ' + index + ', ' + this.getLayerString() );
      
      this.reindex();
      this.getScene().markSceneForRemoval( this, child, index );
    }
  };
  
  return Instance;
} );



// Copyright 2002-2013, University of Colorado

/**
 * An interval between two Trails. A trail being null means either 'from the start' or 'to the end', depending
 * on whether it is the first or second parameter to the constructor.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/RenderInterval',['require','SCENERY/scenery','SCENERY/util/Trail'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/util/Trail' );
  
  // start and end are of type {Trail} or null (indicates all the way to the start / end)
  scenery.RenderInterval = function RenderInterval( start, end ) {
    sceneryAssert && sceneryAssert( !start || !end || start.compare( end ) <= 0, 'RenderInterval parameters must not be out of order' );
    
    this.start = start;
    this.end = end;
  };
  var RenderInterval = scenery.RenderInterval;
  
  // assumes the intervals are disjoint, so we can just compare the start instance
  RenderInterval.compareDisjoint = function( x, y ) {
    // if they are both falsy, they should be the same
    if ( !x.start && !y.start ) { return 0; }
    
    // otherwise, since we are comparing the starts, null would signify 'before anything'
    if ( !x.start || !y.start ) { return x.start ? 1 : -1; }
    
    // otherwise our standard comparison
    return x.start.compare( y.start );
  };
  
  RenderInterval.prototype = {
    constructor: RenderInterval,
    
    reindex: function() {
      this.start && this.start.reindex();
      this.end && this.end.reindex();
    },
    
    isValidExclusive: function() {
      // like construction, but with strict inequality
      return !this.start || !this.end || this.start.compare( this.end ) < 0;
    },
    
    /*
     * Whether the union of this and the specified interval doesn't include any additional trails, when
     * both are treated as exclusive endpoints (exclusive between a and b). We also make the assumption
     * that a !== b || a === null for either interval, since otherwise it is not well defined.
     */
    exclusiveUnionable: function( interval ) {
      sceneryAssert && sceneryAssert ( this.isValidExclusive(), 'exclusiveUnionable requires exclusive intervals' );
      sceneryAssert && sceneryAssert ( interval.isValidExclusive(), 'exclusiveUnionable requires exclusive intervals' );
      return ( !this.start || !interval.end || this.start.compare( interval.end ) === -1 ) &&
             ( !this.end || !interval.start || this.end.compare( interval.start ) === 1 );
    },
    
    exclusiveContains: function( trail ) {
      sceneryAssert && sceneryAssert( trail );
      return ( !this.start || this.start.compare( trail ) < 0 ) && ( !this.end || this.end.compare( trail ) > 0 );
    },
    
    union: function( interval ) {
      // falsy checks since if a or b is null, we want that bound to be null
      var thisA = ( !this.start || ( interval.start && this.start.compare( interval.start ) === -1 ) );
      var thisB = ( !this.end || ( interval.end && this.end.compare( interval.end ) === 1 ) );
      
      return new RenderInterval(
        thisA ? this.start : interval.start,
        thisB ? this.end : interval.end
      );
    },
    
    toString: function() {
      return '[' + ( this.start ? this.start.toString() : this.start ) + ',' + ( this.end ? this.end.toString() : this.end ) + ']';
    }
  };
  
  return RenderInterval;
} );



// Copyright 2002-2013, University of Colorado

/*
 * A pointer is an abstraction that includes a mouse and touch points (and possibly keys).
 *
 * TODO: add state tracking (dragging/panning/etc.) to pointer for convenience
 * TODO: consider an 'active' flag?
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/Pointer',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  scenery.Pointer = function Pointer() {
    this.listeners = [];
    
    phetAllocation && phetAllocation( 'Pointer' );
  };
  var Pointer = scenery.Pointer;
  
  Pointer.prototype = {
    constructor: Pointer,
    
    addInputListener: function( listener ) {
      sceneryAssert && sceneryAssert( !_.contains( this.listeners, listener ) );
      
      this.listeners.push( listener );
    },
    
    removeInputListener: function( listener ) {
      var index = _.indexOf( this.listeners, listener );
      sceneryAssert && sceneryAssert( index !== -1 );
      
      this.listeners.splice( index, 1 );
    }
  };
  
  return Pointer;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Tracks the mouse state
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/Mouse',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/input/Pointer'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );
  
  var Pointer = require( 'SCENERY/input/Pointer' ); // inherits from Pointer
  
  scenery.Mouse = function Mouse() {
    Pointer.call( this );
    
    this.point = null;
    
    this.leftDown = false;
    this.middleDown = false;
    this.rightDown = false;
    
    this.isMouse = true;
    
    this.trail = null;
    
    this.type = 'mouse';
  };
  var Mouse = scenery.Mouse;
  
  inherit( Pointer, Mouse, {
    down: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
      switch( event.button ) {
        case 0: this.leftDown = true; break;
        case 1: this.middleDown = true; break;
        case 2: this.rightDown = true; break;
      }
    },
    
    up: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
      switch( event.button ) {
        case 0: this.leftDown = false; break;
        case 1: this.middleDown = false; break;
        case 2: this.rightDown = false; break;
      }
    },
    
    move: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
    },
    
    over: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
    },
    
    out: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      // TODO: how to handle the mouse out-of-bounds
      this.point = null;
    },
    
    toString: function() {
      return 'Mouse';
    }
  } );
  
  return Mouse;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Tracks a single touch point
 *
 * IE guidelines for Touch-friendly sites: http://blogs.msdn.com/b/ie/archive/2012/04/20/guidelines-for-building-touch-friendly-sites.aspx
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/Touch',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/input/Pointer'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );
  
  var Pointer = require( 'SCENERY/input/Pointer' ); // extends Pointer
  
  scenery.Touch = function Touch( id, point, event ) {
    Pointer.call( this );
    
    this.id = id;
    this.point = point;
    this.isTouch = true;
    this.trail = null;
    
    this.type = 'touch';
  };
  var Touch = scenery.Touch;
  
  inherit( Pointer, Touch, {
    move: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
    },
    
    end: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
    },
    
    cancel: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
    },
    
    toString: function() {
      return 'Touch#' + this.id;
    }
  } );
  
  return Touch;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Tracks a stylus ('pen') or something with tilt and pressure information
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/Pen',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/input/Pointer'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );
  
  var Pointer = require( 'SCENERY/input/Pointer' ); // extends Pointer
  
  scenery.Pen = function Pen( id, point, event ) {
    Pointer.call( this );
    
    this.id = id;
    this.point = point;
    this.isPen = true;
    this.trail = null;
    
    this.type = 'pen';
  };
  var Pen = scenery.Pen;
  
  inherit( Pointer, Pen, {
    move: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
    },
    
    end: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
    },
    
    cancel: function( point, event ) {
      // if ( this.point ) { this.point.freeToPool(); }
      this.point = point;
    },
    
    toString: function() {
      return 'Pen#' + this.id;
    }
  } );
  
  return Pen;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Tracks a single key-press
 *
 * TODO: general key-press implementation
 * TODO: consider separate handling for keys in general.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/Key',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/input/Pointer'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );
  
  var Pointer = require( 'SCENERY/input/Pointer' ); // Inherits from Pointer
  
  scenery.Key = function Key( event ) {
    Pointer.call( this );
    
    this.event = event; // event.keyCode event.charCode
    this.isKey = true; // compared to isMouse/isPen/isTouch
    this.trail = null;
    this.type = 'key';
  };
  var Key = scenery.Key;
  
  inherit( Pointer, Key, {
    
  } );
  
  return Key;
} );

// Copyright 2002-2013, University of Colorado

/*
 * An event in Scenery that has similar event-handling characteristics to DOM events.
 * The original DOM event (if any) is available as event.domEvent.
 *
 * Multiple events can be triggered by a single domEvent, so don't assume it is unique.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */
 
define('SCENERY/input/Event',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  scenery.Event = function Event( args ) {
    // ensure that all of the required args are supplied
    sceneryAssert && sceneryAssert( args.trail &&
                      args.type &&
                      args.pointer &&
                      args.target, 'Missing required scenery.Event argument' );
    
    this.handled = false;
    this.aborted = false;
    
    // {Trail} path to the leaf-most node, ordered list, from root to leaf
    this.trail = args.trail;
    
    // {String} what event was triggered on the listener
    this.type = args.type;
    
    // {Pointer}
    this.pointer = args.pointer;
    
    // raw DOM InputEvent (TouchEvent, PointerEvent, MouseEvent,...)
    this.domEvent = args.domEvent;
    
    // {Node} whatever node you attached the listener to, or null when firing events on a Pointer
    this.currentTarget = args.currentTarget;
    
    // {Node} leaf-most node in trail
    this.target = args.target;
    
    // TODO: add extended information based on an event here?
  };
  var Event = scenery.Event;
  
  Event.prototype = {
    constructor: Event,
    
    // like DOM Event.stopPropagation(), but named differently to indicate it doesn't fire that behavior on the underlying DOM event
    handle: function() {
      this.handled = true;
    },
    
    // like DOM Event.stopImmediatePropagation(), but named differently to indicate it doesn't fire that behavior on the underlying DOM event
    abort: function() {
      this.handled = true;
      this.aborted = true;
    }
  };
  
  return Event;
} );

// Copyright 2002-2013, University of Colorado

/**
 * API for handling mouse / touch / keyboard events.
 *
 * A 'pointer' is an abstract way of describing either the mouse, a single touch point, or a key being pressed.
 * touch points and key presses go away after being released, whereas the mouse 'pointer' is persistent.
 *
 * Events will be called on listeners with a single event object. Supported event types are:
 * 'up', 'down', 'out', 'over', 'enter', 'exit', 'move', and 'cancel'. Scenery also supports more specific event
 * types that constrain the type of pointer, so 'mouse' + type, 'touch' + type and 'pen' + type will fire
 * on each listener before the generic event would be fined. E.g. for mouse movement, listener.mousemove will be
 * fired before listener.move.
 *
 * DOM Level 3 events spec: http://www.w3.org/TR/DOM-Level-3-Events/
 * Touch events spec: http://www.w3.org/TR/touch-events/
 * Pointer events spec draft: https://dvcs.w3.org/hg/pointerevents/raw-file/tip/pointerEvents.html
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/Input',['require','SCENERY/scenery','SCENERY/util/Trail','SCENERY/input/Mouse','SCENERY/input/Touch','SCENERY/input/Pen','SCENERY/input/Key','SCENERY/input/Event'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  require( 'SCENERY/util/Trail' );
  require( 'SCENERY/input/Mouse' );
  require( 'SCENERY/input/Touch' );
  require( 'SCENERY/input/Pen' );
  require( 'SCENERY/input/Key' );
  require( 'SCENERY/input/Event' );
  
  // listenerTarget is the DOM node (window/document/element) to which DOM event listeners will be attached
  scenery.Input = function Input( scene, listenerTarget, batchDOMEvents ) {
    this.scene = scene;
    this.listenerTarget = listenerTarget;
    this.batchDOMEvents = batchDOMEvents;
    
    this.batchedCallbacks = []; // cleared every frame

    //Pointer for mouse, only created lazily on first mouse event, so no mouse is allocated on tablets
    this.mouse = null;

    this.pointers = [];
    
    this.listenerReferences = [];
    
    this.eventLog = [];     // written when recording event input. can be overwritten to the empty array to reset. Strings relative to this class (prefix "scene.input.")
    this.logEvents = false; // can be set to true to cause Scenery to record all input calls to eventLog

    this.pointerAddedListeners = [];
  };
  var Input = scenery.Input;
  
  Input.prototype = {
    constructor: Input,

    addPointer: function( pointer ) {
      this.pointers.push( pointer );

      //Callback for showing pointer events.  Optimized for performance.
      if ( this.pointerAddedListeners.length ) {
        for ( var i = 0; i < this.pointerAddedListeners.length; i++ ) {
          this.pointerAddedListeners[i]( pointer );
        }
      }
    },

    addPointerAddedListener:function(listener){
      this.pointerAddedListeners.push(listener);
    },

    removePointerAddedListener:function(listener){
      var index = this.pointerAddedListeners.indexOf( listener );
      if ( index !== -1 ) {
        this.pointerAddedListeners.splice( index, index + 1 );
      }
    },
    
    removePointer: function( pointer ) {
      // sanity check version, will remove all instances
      for ( var i = this.pointers.length - 1; i >= 0; i-- ) {
        if ( this.pointers[i] === pointer ) {
          this.pointers.splice( i, 1 );
        }
      }
    },
    
    findTouchById: function( id ) {
      var i = this.pointers.length;
      while ( i-- ) {
        var pointer = this.pointers[i];
        if ( pointer.id === id ) {
          return pointer;
        }
      }
      return undefined;
    },
    
    findKeyByEvent: function( event ) {
      sceneryAssert && sceneryAssert( event.keyCode && event.charCode, 'Assumes the KeyboardEvent has keyCode and charCode properties' );
      var result = _.find( this.pointers, function( pointer ) {
        // TODO: also check location (if that exists), so we don't mix up left and right shift, etc.
        return pointer.keyCode === event.keyCode && pointer.charCode === event.charCode;
      } );
      // sceneryAssert && sceneryAssert( result, 'No key found for the combination of key:' + event.key + ' and location:' + event.location );
      return result;
    },

    //Init the mouse on the first mouse event (if any!)
    initMouse: function() {
      this.mouse = new scenery.Mouse();
      this.addPointer( this.mouse );
    },
    
    mouseDown: function( point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'mouseDown(' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      if ( !this.mouse ) { this.initMouse(); }
      this.mouse.down( point, event );
      this.downEvent( this.mouse, event );
    },
    
    mouseUp: function( point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'mouseUp(' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      if ( !this.mouse ) { this.initMouse(); }
      this.mouse.up( point, event );
      this.upEvent( this.mouse, event );
    },
    
    mouseUpImmediate: function( point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'mouseUpImmediate(' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      if ( !this.mouse ) { this.initMouse(); }
      this.upImmediateEvent( this.mouse, event );
    },
    
    mouseMove: function( point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'mouseMove(' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      if ( !this.mouse ) { this.initMouse(); }
      this.mouse.move( point, event );
      this.moveEvent( this.mouse, event );
    },
    
    mouseOver: function( point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'mouseOver(' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      if ( !this.mouse ) { this.initMouse(); }
      this.mouse.over( point, event );
      // TODO: how to handle mouse-over (and log it)
    },
    
    mouseOut: function( point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'mouseOut(' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      if ( !this.mouse ) { this.initMouse(); }
      this.mouse.out( point, event );
      // TODO: how to handle mouse-out (and log it)
    },
    
    keyDown: function( event ) {
      if ( this.logEvents ) { this.eventLog.push( 'keyDown(' + Input.serializeDomEvent( event ) + ');' ); }
      var key = new scenery.Key( event );
      this.addPointer( key );
      
      var trail = this.scene.getTrailFromKeyboardFocus();
      this.dispatchEvent( trail, 'keyDown', key, event, true );
    },
    
    keyUp: function( event ) {
      if ( this.logEvents ) { this.eventLog.push( 'keyUp(' + Input.serializeDomEvent( event ) + ');' ); }
      var key = this.findKeyByEvent( event );
      if ( key ) {
        this.removePointer( key );
        
        var trail = this.scene.getTrailFromKeyboardFocus();
        this.dispatchEvent( trail, 'keyUp', key, event, true );
      }
    },
    
    keyPress: function( event ) {
      if ( this.logEvents ) { this.eventLog.push( 'keyPress(' + Input.serializeDomEvent( event ) + ');' ); }
      // NOTE: do we even need keyPress?
    },
    
    // called for each touch point
    touchStart: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'touchStart(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var touch = new scenery.Touch( id, point, event );
      this.addPointer( touch );
      this.downEvent( touch, event );
    },
    
    touchEnd: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'touchEnd(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var touch = this.findTouchById( id );
      if ( touch ) {
        touch.end( point, event );
        this.removePointer( touch );
        this.upEvent( touch, event );
      } else {
        sceneryAssert && sceneryAssert( false, 'Touch not found for touchEnd: ' + id );
      }
    },
    
    touchEndImmediate: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'touchEndImmediate(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var touch = this.findTouchById( id );
      if ( touch ) {
        this.upImmediateEvent( touch, event );
      } else {
        sceneryAssert && sceneryAssert( false, 'Touch not found for touchEndImmediate: ' + id );
      }
    },
    
    touchMove: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'touchMove(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var touch = this.findTouchById( id );
      if ( touch ) {
        touch.move( point, event );
        this.moveEvent( touch, event );
      } else {
        sceneryAssert && sceneryAssert( false, 'Touch not found for touchMove: ' + id );
      }
    },
    
    touchCancel: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'touchCancel(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var touch = this.findTouchById( id );
      if ( touch ) {
        touch.cancel( point, event );
        this.removePointer( touch );
        this.cancelEvent( touch, event );
      } else {
        sceneryAssert && sceneryAssert( false, 'Touch not found for touchCancel: ' + id );
      }
    },
    
    // called for each touch point
    penStart: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'penStart(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var pen = new scenery.Pen( id, point, event );
      this.addPointer( pen );
      this.downEvent( pen, event );
    },
    
    penEnd: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'penEnd(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var pen = this.findTouchById( id );
      if ( pen ) {
        pen.end( point, event );
        this.removePointer( pen );
        this.upEvent( pen, event );
      } else {
        sceneryAssert && sceneryAssert( false, 'Pen not found for penEnd: ' + id );
      }
    },
    
    penEndImmediate: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'penEndImmediate(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var pen = this.findTouchById( id );
      if ( pen ) {
        this.upImmediateEvent( pen, event );
      } else {
        sceneryAssert && sceneryAssert( false, 'Pen not found for penEndImmediate: ' + id );
      }
    },
    
    penMove: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'penMove(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var pen = this.findTouchById( id );
      if ( pen ) {
        pen.move( point, event );
        this.moveEvent( pen, event );
      } else {
        sceneryAssert && sceneryAssert( false, 'Pen not found for penMove: ' + id );
      }
    },
    
    penCancel: function( id, point, event ) {
      if ( this.logEvents ) { this.eventLog.push( 'penCancel(\'' + id + '\',' + Input.serializeVector2( point ) + ',' + Input.serializeDomEvent( event ) + ');' ); }
      var pen = this.findTouchById( id );
      if ( pen ) {
        pen.cancel( point, event );
        this.removePointer( pen );
        this.cancelEvent( pen, event );
      } else {
        sceneryAssert && sceneryAssert( false, 'Pen not found for penCancel: ' + id );
      }
    },
    
    pointerDown: function( id, type, point, event ) {
      switch ( type ) {
        case 'mouse':
          this.mouseDown( point, event );
          break;
        case 'touch':
          this.touchStart( id, point, event );
          break;
        case 'pen':
          this.penStart( id, point, event );
          break;
        default:
          if ( console.log ) {
            console.log( 'Unknown pointer type: ' + type );
          }
      }
    },
    
    pointerUp: function( id, type, point, event ) {
      switch ( type ) {
        case 'mouse':
          this.mouseUp( point, event );
          break;
        case 'touch':
          this.touchEnd( id, point, event );
          break;
        case 'pen':
          this.penEnd( id, point, event );
          break;
        default:
          if ( console.log ) {
            console.log( 'Unknown pointer type: ' + type );
          }
      }
    },
    
    pointerUpImmediate: function( id, type, point, event ) {
      switch ( type ) {
        case 'mouse':
          this.mouseUpImmediate( point, event );
          break;
        case 'touch':
          this.touchEndImmediate( id, point, event );
          break;
        case 'pen':
          this.penEndImmediate( id, point, event );
          break;
        default:
          if ( console.log ) {
            console.log( 'Unknown pointer type: ' + type );
          }
      }
    },
    
    pointerCancel: function( id, type, point, event ) {
      switch ( type ) {
        case 'mouse':
          if ( console && console.log ) {
            console.log( 'WARNING: Pointer mouse cancel was received' );
          }
          break;
        case 'touch':
          this.touchCancel( id, point, event );
          break;
        case 'pen':
          this.penCancel( id, point, event );
          break;
        default:
          if ( console.log ) {
            console.log( 'Unknown pointer type: ' + type );
          }
      }
    },
    
    pointerMove: function( id, type, point, event ) {
      switch ( type ) {
        case 'mouse':
          this.mouseMove( point, event );
          break;
        case 'touch':
          this.touchMove( id, point, event );
          break;
        case 'pen':
          this.penMove( id, point, event );
          break;
        default:
          if ( console.log ) {
            console.log( 'Unknown pointer type: ' + type );
          }
      }
    },
    
    pointerOver: function( id, type, point, event ) {
      
    },
    
    pointerOut: function( id, type, point, event ) {
      
    },
    
    pointerEnter: function( id, type, point, event ) {
      
    },
    
    pointerLeave: function( id, type, point, event ) {
      
    },
    
    upEvent: function( pointer, event ) {
      var trail = this.scene.trailUnderPointer( pointer ) || new scenery.Trail( this.scene );
      
      this.dispatchEvent( trail, 'up', pointer, event, true );
      
      // touch pointers are transient, so fire exit/out to the trail afterwards
      if ( pointer.isTouch ) {
        this.exitEvents( pointer, event, trail, 0, true );
      }
      
      pointer.trail = trail;
    },
    
    upImmediateEvent: function( pointer, event ) {
      var trail = this.scene.trailUnderPointer( pointer ) || new scenery.Trail( this.scene );
      
      this.dispatchEvent( trail, 'upImmediate', pointer, event, true );
    },
    
    downEvent: function( pointer, event ) {
      var trail = this.scene.trailUnderPointer( pointer ) || new scenery.Trail( this.scene );
      
      // touch pointers are transient, so fire enter/over to the trail first
      if ( pointer.isTouch ) {
        this.enterEvents( pointer, event, trail, 0, true );
      }
      
      this.dispatchEvent( trail, 'down', pointer, event, true );
      
      pointer.trail = trail;
    },
    
    moveEvent: function( pointer, event ) {
      this.branchChangeEvents( pointer, event, true );
    },
    
    cancelEvent: function( pointer, event ) {
      var trail = this.scene.trailUnderPointer( pointer ) || new scenery.Trail( this.scene );
      
      this.dispatchEvent( trail, 'cancel', pointer, event, true );
      
      // touch pointers are transient, so fire exit/out to the trail afterwards
      if ( pointer.isTouch ) {
        this.exitEvents( pointer, event, trail, 0, true );
      }
      
      pointer.trail = trail;
    },
    
    branchChangeEvents: function( pointer, event, isMove ) {
      var trail = this.scene.trailUnderPointer( pointer ) || new scenery.Trail( this.scene );
      var oldTrail = pointer.trail || new scenery.Trail( this.scene ); // TODO: consider a static trail reference
      
      var lastNodeChanged = oldTrail.lastNode() !== trail.lastNode();
      if ( !lastNodeChanged && !isMove ) {
        // bail out if nothing needs to be done
        return;
      }
      
      var branchIndex;
      
      for ( branchIndex = 0; branchIndex < Math.min( trail.length, oldTrail.length ); branchIndex++ ) {
        if ( trail.nodes[branchIndex] !== oldTrail.nodes[branchIndex] ) {
          break;
        }
      }
      
      // event order matches http://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order
      if ( isMove ) {
        this.dispatchEvent( trail, 'move', pointer, event, true );
      }
      
      // we want to approximately mimic http://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order
      // TODO: if a node gets moved down 1 depth, it may see both an exit and enter?
      this.exitEvents( pointer, event, oldTrail, branchIndex, lastNodeChanged );
      this.enterEvents( pointer, event, trail, branchIndex, lastNodeChanged );
      
      pointer.trail = trail;
    },
    
    enterEvents: function( pointer, event, trail, branchIndex, lastNodeChanged ) {
      if ( trail.length > branchIndex ) {
        for ( var newIndex = trail.length - 1; newIndex >= branchIndex; newIndex-- ) {
          this.dispatchEvent( trail.slice( 0, newIndex + 1 ), 'enter', pointer, event, false );
        }
      }
      
      if ( lastNodeChanged ) {
        this.dispatchEvent( trail, 'over', pointer, event, true );
      }
    },
    
    exitEvents: function( pointer, event, trail, branchIndex, lastNodeChanged ) {
      if ( lastNodeChanged ) {
        this.dispatchEvent( trail, 'out', pointer, event, true );
      }
      
      if ( trail.length > branchIndex ) {
        for ( var oldIndex = branchIndex; oldIndex < trail.length; oldIndex++ ) {
          this.dispatchEvent( trail.slice( 0, oldIndex + 1 ), 'exit', pointer, event, false );
        }
      }
    },
    
    validatePointers: function() {
      var that = this;
      
      var i = this.pointers.length;
      while ( i-- ) {
        var pointer = this.pointers[i];
        if ( pointer.point ) {
          that.branchChangeEvents( pointer, null, false );
        }
      }
    },
    
    dispatchEvent: function( trail, type, pointer, event, bubbles ) {
      sceneryEventLog && sceneryEventLog( 'Input: ' + type + ' on ' + trail.toString() + ' for pointer ' + pointer.toString() );
      if ( !trail ) {
        try {
          throw new Error( 'falsy trail for dispatchEvent' );
        } catch ( e ) {
          console.log( e.stack );
          throw e;
        }
      }
      
      // TODO: is there a way to make this event immutable?
      var inputEvent = new scenery.Event( {
        trail: trail, // {Trail} path to the leaf-most node, ordered list, from root to leaf
        type: type, // {String} what event was triggered on the listener
        pointer: pointer, // {Pointer}
        domEvent: event, // raw DOM InputEvent (TouchEvent, PointerEvent, MouseEvent,...)
        currentTarget: null, // {Node} whatever node you attached the listener to, null when passed to a Pointer,
        target: trail.lastNode() // {Node} leaf-most node in trail
      } );
      
      // first run through the pointer's listeners to see if one of them will handle the event
      this.dispatchToPointer( type, pointer, inputEvent );
      
      // if not yet handled, run through the trail in order to see if one of them will handle the event
      // at the base of the trail should be the scene node, so the scene will be notified last
      this.dispatchToTargets( trail, pointer, type, inputEvent, bubbles );
      
      // TODO: better interactivity handling?
      if ( !trail.lastNode().interactive && !pointer.isKey && event && event.preventDefault ) {
        event.preventDefault();
      }
    },
    
    // TODO: reduce code sharing between here and dispatchToTargets!
    dispatchToPointer: function( type, pointer, inputEvent ) {
      if ( inputEvent.aborted || inputEvent.handled ) {
        return;
      }
      
      var specificType = pointer.type + type; // e.g. mouseup, touchup, keyup
      
      var pointerListeners = pointer.listeners.slice( 0 ); // defensive copy
      for ( var i = 0; i < pointerListeners.length; i++ ) {
        var listener = pointerListeners[i];
        
        // if a listener returns true, don't handle any more
        var aborted = false;
        
        if ( !aborted && listener[specificType] ) {
          listener[specificType]( inputEvent );
          aborted = inputEvent.aborted;
        }
        if ( !aborted && listener[type] ) {
          listener[type]( inputEvent );
          aborted = inputEvent.aborted;
        }
        
        // bail out if the event is aborted, so no other listeners are triggered
        if ( aborted ) {
          return;
        }
      }
    },
    
    dispatchToTargets: function( trail, pointer, type, inputEvent, bubbles ) {
      if ( inputEvent.aborted || inputEvent.handled ) {
        return;
      }
      
      var specificType = pointer.type + type; // e.g. mouseup, touchup, keyup
      
      for ( var i = trail.length - 1; i >= 0; bubbles ? i-- : i = -1 ) {
        var target = trail.nodes[i];
        inputEvent.currentTarget = target;
        
        var listeners = target.getInputListeners();
        
        for ( var k = 0; k < listeners.length; k++ ) {
          var listener = listeners[k];
          
          // if a listener returns true, don't handle any more
          var aborted = false;
          
          if ( !aborted && listener[specificType] ) {
            listener[specificType]( inputEvent );
            aborted = inputEvent.aborted;
          }
          if ( !aborted && listener[type] ) {
            listener[type]( inputEvent );
            aborted = inputEvent.aborted;
          }
          
          // bail out if the event is aborted, so no other listeners are triggered
          if ( aborted ) {
            return;
          }
        }
        
        // if the input event was handled, don't follow the trail down another level
        if ( inputEvent.handled ) {
          return;
        }
      }
    },
    
    addListener: function( type, callback, useCapture ) {
      var input = this;
      
      //Cancel propagation of mouse events but not key events.  Key Events need to propagate for tab navigability
      var usePreventDefault = type !== 'keydown' && type !== 'keyup' && type !== 'keypress';
      
      if ( this.batchDOMEvents ) {
        var batchedCallback = function batchedEvent( domEvent ) {
          sceneryEventLog && sceneryEventLog( 'Batching event for ' + type );
          
          if ( usePreventDefault ) {
            domEvent.preventDefault(); // TODO: should we batch the events in a different place so we don't preventDefault on something bad?
          }
          input.batchedCallbacks.push( function batchedEventCallback() {
            // process whether anything under the pointers changed before running additional input events
            input.validatePointers();
            if ( input.logEvents ) { input.eventLog.push( 'validatePointers();' ); }
            
            callback( domEvent );
          } );
        };
        this.listenerTarget.addEventListener( type, batchedCallback, useCapture );
        this.listenerReferences.push( { type: type, callback: batchedCallback, useCapture: useCapture } );
      } else {
        this.listenerTarget.addEventListener( type, callback, useCapture );
        this.listenerReferences.push( { type: type, callback: function synchronousEvent( domEvent ) {
          sceneryEventLog && sceneryEventLog( 'Running event for ' + type );
          
          // process whether anything under the pointers changed before running additional input events
          input.validatePointers();
          if ( input.logEvents ) { input.eventLog.push( 'validatePointers();' ); }
          
          callback( domEvent );
        }, useCapture: useCapture } );
      }
    },
    
    // temporary, for mouse events
    addImmediateListener: function( type, callback, useCapture ) {
      var input = this;
      
      this.listenerTarget.addEventListener( type, callback, useCapture );
      this.listenerReferences.push( { type: type, callback: function immediateEvent( domEvent ) {
        sceneryEventLog && sceneryEventLog( 'Running immediate event for ' + type );
        
        // process whether anything under the pointers changed before running additional input events
        // input.validatePointers();
        // if ( input.logEvents ) { input.eventLog.push( 'validatePointers();' ); }
        
        callback( domEvent );
      }, useCapture: useCapture } );
    },
    
    disposeListeners: function() {
      var input = this;
      _.each( this.listenerReferences, function( ref ) {
        input.listenerTarget.removeEventListener( ref.type, ref.callback, ref.useCapture );
      } );
    },
    
    fireBatchedEvents: function() {
      if ( this.batchedCallbacks.length ) {
        sceneryEventLog && sceneryEventLog( 'Input.fireBatchedEvents length:' + this.batchedCallbacks.length );
        var len = this.batchedCallbacks.length;
        for ( var i = 0; i < len; i++ ) {
          this.batchedCallbacks[i]();
        }
        this.batchedCallbacks.length = 0;
      }
    }
  };
  
  Input.serializeDomEvent = function serializeDomEvent( domEvent ) {
    var lines = [];
    for ( var prop in domEvent ) {
      if ( domEvent.hasOwnProperty( prop ) ) {
        // stringifying dom event object properties can cause circular references, so we avoid that completely
        if ( prop === 'touches' || prop === 'targetTouches' || prop === 'changedTouches' ) {
          var arr = [];
          for ( var i = 0; i < domEvent[prop].length; i++ ) {
            // according to spec (http://www.w3.org/TR/touch-events/), this is not an Array, but a TouchList
            var touch = domEvent[prop].item( i );
            
            arr.push( serializeDomEvent( touch ) );
          }
          lines.push( prop + ':[' + arr.join( ',' ) + ']' );
        } else {
          lines.push( prop + ':' + ( ( typeof domEvent[prop] === 'object' ) && ( domEvent[prop] !== null ) ? '{}' : JSON.stringify( domEvent[prop] ) ) );
        }
      }
    }
    return '{' + lines.join( ',' ) + '}';
  };
  
  Input.serializeVector2 = function( vector ) {
    return 'dot(' + vector.x + ',' + vector.y + ')';
  };
  
  return Input;
} );

// Copyright 2002-2013, University of Colorado

/**
 * A conceptual boundary between layers, where it is optional to have information about a previous or next layer.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/LayerBoundary',['require','SCENERY/scenery'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  scenery.LayerBoundary = function LayerBoundary() {
    // layer types before and after the boundary. null indicates the lack of information (first or last layer)
    this.previousLayerType = null;
    this.nextLayerType = null;
    
    // trails to the closest nodes with isPainted() === true before and after the boundary
    this.previousPaintedTrail = null;
    this.nextPaintedTrail = null;
  };
  var LayerBoundary = scenery.LayerBoundary;
  
  LayerBoundary.prototype = {
    constructor: LayerBoundary,
    
    hasPrevious: function() {
      return !!this.previousPaintedTrail;
    },
    
    hasNext: function() {
      return !!this.nextPaintedTrail;
    },
    
    // reindexes the trails
    reindex: function() {
      this.previousPaintedTrail && this.previousPaintedTrail.reindex();
      this.nextPaintedTrail && this.nextPaintedTrail.reindex();
    },
    
    // assumes that trail is reindexed
    equivalentPreviousTrail: function( trail ) {
      if ( this.previousPaintedTrail && trail ) {
        this.previousPaintedTrail.reindex();
        return this.previousPaintedTrail.equals( trail );
      } else {
        // check that handles null versions properly
        return this.previousPaintedTrail === trail;
      }
    },
    
    equivalentNextTrail: function( trail ) {
      if ( this.nextPaintedTrail && trail ) {
        this.nextPaintedTrail.reindex();
        return this.nextPaintedTrail.equals( trail );
      } else {
        // check that handles null versions properly
        return this.nextPaintedTrail === trail;
      }
    },
    
    toString: function() {
      return 'boundary:' +
             '\n    types:    ' +
                  ( this.previousLayerType ? this.previousLayerType.name : '' ) +
                  ' => ' +
                  ( this.nextLayerType ? this.nextLayerType.name : '' ) +
             '\n    trails:   ' +
                  ( this.previousPaintedTrail ? this.previousPaintedTrail.getUniqueId() : '' ) +
                  ' => ' +
                  ( this.nextPaintedTrail ? this.nextPaintedTrail.getUniqueId() : '' );
    }
  };
  
  return LayerBoundary;
} );

// Copyright 2002-2013, University of Colorado

/**
 * A layer state is used to construct layer information (and later, layers), and is a state machine
 * that layer strategies from each node modify. Iterating through all of the nodes in a depth-first
 * manner will modify the LayerBuilder so that layer information can be retrieved.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/layers/LayerBuilder',['require','SCENERY/scenery','SCENERY/layers/LayerBoundary','SCENERY/util/Trail','SCENERY/util/TrailPointer'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/layers/LayerBoundary' );
  require( 'SCENERY/util/Trail' );
  require( 'SCENERY/util/TrailPointer' );
  
  /*
   * Builds layer information between trails
   *
   * previousLayerType should be null if there is no previous layer.
   */
  scenery.LayerBuilder = function LayerBuilder( scene, previousLayerType, previousPaintedTrail, nextPaintedTrail ) {
    
    /*---------------------------------------------------------------------------*
    * Initial state
    *----------------------------------------------------------------------------*/
    
    this.layerTypeStack = [];
    this.boundaries = [];
    this.pendingBoundary = new scenery.LayerBoundary();
    this.pendingBoundary.previousLayerType = previousLayerType;
    this.pendingBoundary.previousPaintedTrail = previousPaintedTrail;
    
    /*
     * The current layer type active, and whether it has been 'used' yet. A node with isPainted() will trigger a 'used' action,
     * and if the layer hasn't been used, it will actually trigger a boundary creation. We want to collapse 'unused' layers
     * and boundaries together, so that every created layer has a node that displays something.
     */
    this.currentLayerType = previousLayerType;
    this.layerChangePending = previousPaintedTrail === null;
    
    /*---------------------------------------------------------------------------*
    * Start / End pointers
    *----------------------------------------------------------------------------*/
    
    if ( previousPaintedTrail ) {
      // Move our start pointer just past the previousPaintedTrail, since our previousLayerType is presumably for that trail's node's self.
      // Anything after that self could have been collapsed, so we need to start there.
      this.startPointer = new scenery.TrailPointer( previousPaintedTrail.copy(), true );
      this.startPointer.nestedForwards();
    } else {
      this.startPointer = new scenery.TrailPointer( new scenery.Trail( scene ), true );
    }
    
    if ( nextPaintedTrail ) {
      // include the nextPaintedTrail's 'before' in our iteration, so we can stitch properly with the next layer
      this.endPointer = new scenery.TrailPointer( nextPaintedTrail.copy(), true );
    } else {
      this.endPointer = new scenery.TrailPointer( new scenery.Trail( scene ), false );
    }
    
    this.includesEndTrail = nextPaintedTrail !== null;
    
    /*
     * LayerBoundary properties and assurances:
     *
     * previousLayerType  - initialized in constructor (in case there are no layer changes)
     *                      set in layerChange for "fresh" pending boundary
     * nextLayerType      - set and overwrites in switchToType, for collapsing layers
     *                      not set anywhere else, so we can leave it null
     * previousPaintedTrail  - initialized in constructor
     *                      updated in markPainted if there is no pending change (don't set if there is a pending change)
     * nextPaintedTrail      - set on layerChange for "stale" boundary
     *                      stays null if nextPaintedTrail === null
     */
  };
  var LayerBuilder = scenery.LayerBuilder;
  
  LayerBuilder.prototype = {
    constructor: LayerBuilder,
    
    // walks part of the state up to just before the startPointer. we want the preferred layer stack to be in place, but the rest is not important
    prepareLayerStack: function() {
      var pointer = new scenery.TrailPointer( new scenery.Trail( this.startPointer.trail.rootNode() ), true );
      
      // if the start pointer is going to execute an exit() instead of an enter() on its trail node, we need to bump up the layer stack an additional step
      var targetLength = this.startPointer.trail.length - ( this.startPointer.isBefore ? 1 : 0 );
      
      while ( pointer.trail.length <= targetLength ) {
        var node = pointer.trail.lastNode();
        if ( node.layerStrategy.hasPreferredLayerType( pointer, this ) ) {
          this.pushPreferredLayerType( node.layerStrategy.getPreferredLayerType( pointer, this ) );
        }
        if ( pointer.trail.length < this.startPointer.trail.nodes.length ) {
          pointer.trail.addDescendant( this.startPointer.trail.nodes[pointer.trail.length] );
        } else {
          // TODO: a cleaner way of doing this, since we were adding undefined into a trail to cause the loop to exit
          break;
        }
      }
    },
    
    run: function() {
      var builder = this;
      
      // push preferred layers for ancestors of our start pointer
      this.prepareLayerStack();
      
      // console.log( '         stack: ' + _.map( builder.layerTypeStack, function( type ) { return type.name; } ).join( ', ' ) );
      
      builder.startPointer.depthFirstUntil( builder.endPointer, function( pointer ) {
        var node = pointer.trail.lastNode();
        
        if ( pointer.isBefore ) {
          // console.log( 'builder: enter ' + pointer.toString() );
          node.layerStrategy.enter( pointer, builder );
        } else {
          // console.log( 'builder: exit ' + pointer.toString() );
          node.layerStrategy.exit( pointer, builder );
        }
        // console.log( '         stack: ' + _.map( builder.layerTypeStack, function( type ) { return type.name; } ).join( ', ' ) );
      }, false ); // include the endpoints
      
      // special case handling if we are at the 'end' of the scene, so that we create another 'wrapping' boundary
      if ( !this.includesEndTrail ) {
        // console.log( 'builder: not including end trail' );
        this.layerChange( null );
      }
    },
    
    // allows paintedPointer === null at the end if the main iteration's nextPaintedTrail === null (i.e. we are at the end of the scene)
    layerChange: function( paintedPointer ) {
      this.layerChangePending = false;
      
      var confirmedBoundary = this.pendingBoundary;
      
      confirmedBoundary.nextPaintedTrail = paintedPointer ? paintedPointer.trail.copy() : null;
      
      this.boundaries.push( confirmedBoundary );
      
      this.pendingBoundary = new scenery.LayerBoundary();
      this.pendingBoundary.previousLayerType = confirmedBoundary.nextLayerType;
      this.pendingBoundary.previousPaintedTrail = confirmedBoundary.nextPaintedTrail;
      // console.log( 'builder:   added boundary' );
    },
    
    /*---------------------------------------------------------------------------*
    * API for layer strategy or other interaction
    *----------------------------------------------------------------------------*/
    
    switchToType: function( pointer, layerType ) {
      this.currentLayerType = layerType;
      
      this.pendingBoundary.nextLayerType = layerType;
      
      this.layerChangePending = true; // we wait until the first markPainted() call to create a boundary
    },
    
    // called so that we can finalize a layer switch (instead of collapsing unneeded layers)
    markPainted: function( pointer ) {
      if ( this.layerChangePending ) {
        this.layerChange( pointer );
      } else {
        // TODO: performance-wise, don't lookup indices on this copy? make a way to create a lightweight copy?
        this.pendingBoundary.previousPaintedTrail = pointer.trail.copy();
      }
    },
    
    // can be null to indicate that there is no current layer type
    getCurrentLayerType: function() {
      return this.currentLayerType;
    },
    
    pushPreferredLayerType: function( layerType ) {
      this.layerTypeStack.push( layerType );
    },
    
    popPreferredLayerType: function() {
      this.layerTypeStack.pop();
    },
    
    getPreferredLayerType: function() {
      if ( this.layerTypeStack.length !== 0 ) {
        return this.layerTypeStack[this.layerTypeStack.length - 1];
      } else {
        return null;
      }
    },
    
    bestPreferredLayerTypeFor: function( renderers ) {
      for ( var i = this.layerTypeStack.length - 1; i >= 0; i-- ) {
        var preferredType = this.layerTypeStack[i];
        
        var k = renderers.length;
        while ( k-- ) {
          // if any renderer is supported by this type, use this type
          if ( preferredType.supportsRenderer( renderers[k] ) ) {
            return preferredType;
          }
        }
      }
      
      // none of our stored preferred layer types are able to support any of the default type options
      return null;
    }
  };
  
  return LayerBuilder;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * The PointerOverlay shows pointer locations in the scene.  This is useful when recording a session for interviews or when a teacher is broadcasting
 * a tablet session on an overhead projector.  See https://github.com/phetsims/scenery/issues/111
 *
 * Each pointer is rendered in a different <svg> so that CSS3 transforms can be used to make performance smooth on iPad.
 *
 * @author Sam Reid
 */

define('SCENERY/overlays/PointerOverlay',['require','DOT/Bounds2','DOT/Transform3','DOT/Matrix3','SCENERY/scenery','SCENERY/util/Trail','SCENERY/util/Util'], function( require ) {
  

  var Bounds2 = require( 'DOT/Bounds2' );
  var Transform3 = require( 'DOT/Transform3' );
  var Matrix3 = require( 'DOT/Matrix3' );

  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/util/Trail' );

  var Util = require( 'SCENERY/util/Util' );

  scenery.PointerOverlay = function PointerOverlay( scene ) {
    var pointerOverlay = this;
    this.scene = scene;

    // add element to show the pointers
    this.pointerSVGContainer = document.createElement( 'div' );
    this.pointerSVGContainer.style.position = 'absolute';
    this.pointerSVGContainer.style.top = 0;
    this.pointerSVGContainer.style.left = 0;
    this.pointerSVGContainer.style['pointer-events'] = 'none';

    var innerRadius = 10;
    var strokeWidth = 1;
    var diameter = (innerRadius + strokeWidth / 2) * 2;
    var radius = diameter / 2;

    //Resize the parent div when the scene is resized
    scene.addEventListener( 'resize', function( args ) {
      pointerOverlay.pointerSVGContainer.setAttribute( 'width', args.width );
      pointerOverlay.pointerSVGContainer.setAttribute( 'height', args.height );
      pointerOverlay.pointerSVGContainer.style.clip = 'rect(0px,' + args.width + 'px,' + args.height + 'px,0px)';
    }, false );

    //Display a pointer that was added.  Use a separate SVG layer for each pointer so it can be hardware accelerated, otherwise it is too slow just setting svg internal attributes
    var pointerAdded = function( pointer ) {

      if ( pointer.isKey ) { return; }

      var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
      svg.style.position = 'absolute';
      svg.style.top = 0;
      svg.style.left = 0;
      svg.style['pointer-events'] = 'none';

      //Fit the size to the display
      svg.setAttribute( 'width', diameter );
      svg.setAttribute( 'height', diameter );

      var circle = document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' );

      //use css transform for performance?
      circle.setAttribute( 'cx', innerRadius + strokeWidth / 2 );
      circle.setAttribute( 'cy', innerRadius + strokeWidth / 2 );
      circle.setAttribute( 'r', innerRadius );
      circle.setAttribute( 'style', 'fill:black;' );
      circle.setAttribute( 'opacity', 0.4 );

      //Add a move listener to the pointer to update position when it has moved
      var pointerRemoved = function() {

        //For touches that get a touch up event, remove them.  But when the mouse button is released, don't stop showing the mouse location
        if ( pointer.isTouch ) {
          pointerOverlay.pointerSVGContainer.removeChild( svg );
          pointer.removeInputListener( moveListener );
        }
      };
      var moveListener = {
        move: function() {

          //TODO: Why is point sometimes null?
          if ( pointer.point ) {

            //TODO: this allocates memory when pointers are dragging, perhaps rewrite to remove allocations
            Util.applyCSSTransform( Matrix3.translation( pointer.point.x - radius, pointer.point.y - radius ), svg );
          }
        },

        up: pointerRemoved,
        cancel: pointerRemoved
      };
      pointer.addInputListener( moveListener );

      moveListener.move();
      svg.appendChild( circle );
      pointerOverlay.pointerSVGContainer.appendChild( svg );
    };
    scene.input.addPointerAddedListener( pointerAdded );

    //if there is already a mouse, add it here
    //TODO: if there already other non-mouse touches, could be added here
    if ( scene.input && scene.input.mouse ) {
      pointerAdded( scene.input.mouse );
    }

    scene.reindexLayers();
    scene.$main[0].appendChild( this.pointerSVGContainer );
  };
  var PointerOverlay = scenery.PointerOverlay;

  PointerOverlay.prototype = {
    dispose: function() {
      this.scene.$main[0].removeChild( this.pointerSVGContainer );
      delete this.scene.input.pointerListener;
    },

    setZIndex: function( index ) {
      this.pointerSVGContainer.style.zIndex = index;//Make sure it is in front of enough other things!
    }
  };

  return PointerOverlay;
} );
// Copyright 2002-2013, University of Colorado

/**
 * Main scene, that is also a Node.
 *
 * TODO: documentation!
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/Scene',['require','PHET_CORE/collect','PHET_CORE/inherit','DOT/Bounds2','DOT/Vector2','DOT/Matrix3','KITE/Shape','SCENERY/scenery','SCENERY/nodes/Node','SCENERY/util/Instance','SCENERY/util/Trail','SCENERY/util/RenderInterval','SCENERY/util/TrailPointer','SCENERY/input/Input','SCENERY/layers/LayerBuilder','SCENERY/layers/Renderer','SCENERY/overlays/PointerOverlay','SCENERY/util/Util'], function( require ) {
  
  
  var collect = require( 'PHET_CORE/collect' );
  var inherit = require( 'PHET_CORE/inherit' );
  
  var Bounds2 = require( 'DOT/Bounds2' );
  var Vector2 = require( 'DOT/Vector2' );
  var Matrix3 = require( 'DOT/Matrix3' );
  
  var Shape = require( 'KITE/Shape' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  var Node = require( 'SCENERY/nodes/Node' ); // inherits from Node
  require( 'SCENERY/util/Instance' );
  require( 'SCENERY/util/Trail' );
  require( 'SCENERY/util/RenderInterval' );
  require( 'SCENERY/util/TrailPointer' );
  require( 'SCENERY/input/Input' );
  require( 'SCENERY/layers/LayerBuilder' );
  require( 'SCENERY/layers/Renderer' );
  require( 'SCENERY/overlays/PointerOverlay' );

  var Util = require( 'SCENERY/util/Util' );
  var objectCreate = Util.objectCreate;
  
  var accessibility = window.has && window.has( 'scenery.accessibility' );
  
  // debug flag to disable matching of layers when in 'match' mode
  var forceNewLayers = true; // DEBUG
  
  // constructs all sub-trees for the specified instance. used from markSceneForInsertion
  function buildInstances( instance ) {
    var node = instance.getNode();
    var len = node._children.length;
    for ( var i = 0; i < len; i++ ) {
      buildInstances( instance.createChild( node._children[i], i ) );
    }
  }
  
  /*
   * $main should be a block-level element with a defined width and height. scene.resize() should be called whenever
   * it is resized.
   *
   * Valid parameters in the parameter object:
   * {
   *   allowSceneOverflow: false,           // usually anything displayed outside of this $main (DOM/CSS3 transformed SVG) is hidden with CSS overflow
   *   allowCSSHacks: true,                 // applies styling that prevents mobile browser graphical issues
   *   allowDevicePixelRatioScaling: false, // allows underlying canvases (Canvas, WebGL) to increase in size to maintain sharpness on high-density displays
   *   enablePointerEvents: true,           // allows pointer events / MSPointerEvent to be used on supported platforms.
   *   preferredSceneLayerType: ...,        // sets the preferred type of layer to be created if there are multiple options
   *   width: <current main width>,         // override the main container's width
   *   height: <current main height>,       // override the main container's height
   * }
   */
  scenery.Scene = function Scene( $main, options ) {
    sceneryAssert && sceneryAssert( $main[0], 'A main container is required for a scene' );
    this.$main = $main;
    this.main = $main[0];
    
    // add a self reference to aid in debugging. this generally shouldn't lead to a memory leak
    this.main.scene = this;
    
    // add a reference to the API for debugging
    this.scenery = scenery;
    
    // defaults
    options = _.extend( {
      allowSceneOverflow: false,
      allowCSSHacks: true,
      allowDevicePixelRatioScaling: false,
      enablePointerEvents: true,
      preferredSceneLayerType: scenery.CanvasDefaultLayerType,
      width: $main.width(),
      height: $main.height()
    }, options || {} );
    
    // TODO: consider using a pushed preferred layer to indicate this information, instead of as a specific option
    this.backingScale = options.allowDevicePixelRatioScaling ? Util.backingScale( document.createElement( 'canvas' ).getContext( '2d' ) ) : 1;
    this.enablePointerEvents = options.enablePointerEvents;
    
    Node.call( this, options );

    var scene = this;
    window.debugScene = scene;
    
    // layering data
    this.layers = [];               // main layers in a scene
    this.layerChangeIntervals = []; // array of {RenderInterval}s indicating what parts need to be stitched together. cleared after each stitching
    
    this.lastCursor = null;
    this.defaultCursor = $main.css( 'cursor' );
    
    // resize the main container as a sanity check
    this.setSize( options.width, options.height );
    
    this.sceneBounds = new Bounds2( 0, 0, options.width, options.height );
    
    // set up the root instance for this scene
    // only do this after Node.call has been invoked, since Trail.addDescendant uses a few things
    this.rootInstance = new scenery.Instance( new scenery.Trail( this ), null, null );
    this.addInstance( this.rootInstance );
    
    // default to a canvas layer type, but this can be changed
    this.preferredSceneLayerType = options.preferredSceneLayerType;
    
    applyCSSHacks( $main, options );

    // TODO: Does this need to move to where inputEvents are hooked up so that it doesn't get run each time Node.toImage is called?
    if ( accessibility ) {
      this.activePeer = null;
      
      this.accessibilityLayer = document.createElement( 'div' );
      this.accessibilityLayer.className = "accessibility-layer";
      
      //Put the accessibility layer behind the background so it cannot be seen.  Change this to some high number like 9999 to show it for debugging purposes.
      this.accessibilityLayer.style.zIndex = -1;
      this.accessibilityLayer.style.position = 'relative';
      $main[0].appendChild( this.accessibilityLayer );
      
      this.focusRingSVGContainer = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
      this.focusRingSVGContainer.style.position = 'absolute';
      this.focusRingSVGContainer.style.top = 0;
      this.focusRingSVGContainer.style.left = 0;
      this.focusRingSVGContainer.style['pointer-events'] = 'none';
      this.resizeFocusRingSVGContainer( options.width, options.height );
      this.focusRingPath = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
      this.focusRingPath.setAttribute( 'style', 'fill: none; stroke: blue; stroke-width: 5;' );
      this.focusRingPath.setAttribute( 'id', 'p1' );
      this.focusRingSVGContainer.appendChild( this.focusRingPath );
      $main[0].appendChild( this.focusRingSVGContainer );
      
      this.updateFocusRing = function() {
        // TODO: move into prototype definitions, this doesn't need to be private, and isn't a closure over anything in the constructor
        sceneryAssert && sceneryAssert( scene.activePeer, 'scene should have an active peer when changing the focus ring bounds' );
        scene.focusRingPath.setAttribute( 'd', Shape.bounds( scene.activePeer.getGlobalBounds() ).getSVGPath() );
      };

      //Put the live region layer behind the accessibility peer layer to make debugging easier (if we need to see the accessibility layer)
      this.liveRegionLayer = document.createElement( 'div' );
      this.liveRegionLayer.className = 'live-region-layer';
      this.liveRegionLayer.style.zIndex = -2;
      this.liveRegionLayer.style.position = 'relative';
      $main[0].appendChild( this.liveRegionLayer );
    }
  };
  var Scene = scenery.Scene;
  
  inherit( Node, Scene, {
    
    updateScene: function( args ) {
      // sceneryLayerLog && sceneryLayerLog( 'Scene: updateScene' );
      
      var scene = this;
      
      // check to see whether contents under pointers changed (and if so, send the enter/exit events) to maintain consistent state
      if ( this.input ) {
        this.input.validatePointers();
      }
      
      // validating bounds, similar to Piccolo2d
      this.validateBounds();
      this.validatePaint();
      
      // bail if there are no layers. consider a warning?
      if ( !this.layers.length ) {
        return;
      }
      
      var i = this.layers.length;
      while ( i-- ) {
        this.layers[i].render( scene, args );
      }
      
      this.updateCursor();
      
      // if ( this.accessibilityLayer ) {
  //      for ( var i = 0; i < accessibleNodes.length; i++ ) {
  //        if ( accessibleNodes[i]._element === activeElement ) {
  //          if ( accessibleNodes[i].origin ) {
  //            var b = accessibleNodes[i].origin.globalBounds;
  //            var rect = Shape.bounds( b );
  //
  //            //Animation is a bit buggy, but I left this code in in case we want to pick it up later.
  //            var animateTheRect = false;
  //            if ( animateTheRect ) {
  //              if ( !this.focusRingPath.lastSVGPath ) {
  //                this.focusRingPath.setAttribute( 'd', rect.getSVGPath() );
  //                this.focusRingPath.lastSVGPath = rect.getSVGPath();
  //              } else {
  //                var animate = document.createElementNS( 'http://www.w3.org/2000/svg', 'animate' );
  //                animate.setAttribute( 'attributeType', 'XML' );
  //                animate.setAttribute( 'xlink:href', '#p1' );
  //                animate.setAttribute( 'attributeName', 'd' );
  //                animate.setAttribute( 'from', this.focusRingPath.lastSVGPath );
  //                animate.setAttribute( 'to', rect.getSVGPath() );
  //                animate.setAttribute( 'dur', '4s' );
  //
  //                $( this.focusRingPath ).empty();
  //                this.focusRingPath.appendChild( animate );
  //                this.focusRingPath.lastSVGPath = rect.getSVGPath();
  //              }
  //            } else {
  //              this.focusRingPath.setAttribute( 'd', rect.getSVGPath() );
  //            }
  //
  //            found = true;
  //          }
  //        }
  //        if ( !found ) {
  //          this.focusRingPath.removeAttribute( 'd' );
  //        }
  //      }
      // }
    },
    
    renderScene: function() {
      // TODO: for now, go with the same path. possibly add options later
      this.updateScene();
    },
    
    addPeer: function( peer ) {
      this.accessibilityLayer.appendChild( peer.peerElement );
    },
    
    removePeer: function( peer ) {
      this.accessibilityLayer.removeChild( peer.peerElement );
    },

    addLiveRegion: function( liveRegion ) {
      this.liveRegionLayer.appendChild( liveRegion.element );
    },

    removeLiveRegion: function( liveRegion ) {
      this.liveRegionLayer.removeChild( liveRegion.element );
    },
    
    setActivePeer: function( peer ) {
      if ( this.activePeer !== peer ) {
        var scene = this;
        
        //Remove bounds listener from old active peer
        if ( this.activePeer ) {
          this.activePeer.instance.node.removeEventListener( 'bounds', this.updateFocusRing );
        }
        
        this.activePeer = peer;
        
        if ( peer ) {
          this.activePeer.instance.node.addEventListener( 'bounds', this.updateFocusRing );
          this.updateFocusRing();
        } else {
          this.focusRingPath.setAttribute( 'd', "M 0 0" );
        }
      }
    },
    
    getActivePeer: function( peer ) {
      return this.activePeer;
    },
    
    focusPeer: function( peer ) {
      this.setActivePeer( peer );
    },
    
    blurPeer: function( peer ) {
      sceneryAssert && sceneryAssert( this.getActivePeer() === peer, 'Can only blur an active peer' );
      this.setActivePeer( null );
    },
    
    markInterval: function( affectedTrail ) {
      // TODO: maybe reindexing sooner is better? are we covering up a bug here?
      affectedTrail.reindex();
      
      // since this is marked while the child is still connected, we can use our normal trail handling.
      
      // find the closest before and after self trails that are not affected
      var beforeTrail = affectedTrail.previousPainted(); // easy for the before trail
      
      var afterTrailPointer = new scenery.TrailPointer( affectedTrail.copy(), false );
      while ( afterTrailPointer.hasTrail() && ( !afterTrailPointer.isBefore || !afterTrailPointer.trail.isPainted() ) ) {
        afterTrailPointer.nestedForwards();
      }
      var afterTrail = afterTrailPointer.trail;
      
      // sanity checks
      sceneryAssert && sceneryAssert( !beforeTrail || beforeTrail.areIndicesValid(), 'beforeTrail needs to be valid' );
      sceneryAssert && sceneryAssert( !afterTrail || afterTrail.areIndicesValid(), 'afterTrail needs to be valid' );
      sceneryAssert && sceneryAssert( !beforeTrail || !afterTrail || beforeTrail.compare( afterTrail ) !== 0, 'Marked interval needs to be exclusive' );
      
      // store the layer of the before/after trails so that it is easy to access later
      this.addLayerChangeInterval( new scenery.RenderInterval( beforeTrail, afterTrail ) );
    },
    
    // convenience function for layer change intervals
    addLayerChangeInterval: function( interval ) {
      if ( sceneryLayerLog ) {
        sceneryLayerLog( 'adding interval: ' + interval.toString() + ' to intervals:' );
        _.each( this.layerChangeIntervals, function( interval ) {
          sceneryLayerLog( '  ' + interval.toString() );
        } );
      }
      
      // TODO: replace with a binary-search-like version that may be faster. this includes a full scan
      
      // attempt to merge this interval with another if possible.
      for ( var i = 0; i < this.layerChangeIntervals.length; i++ ) {
        var other = this.layerChangeIntervals[i];
        other.reindex(); // sanity check, although for most use-cases this should be unnecessary
        
        if ( interval.exclusiveUnionable( other ) ) {
          // the interval can be unioned without including other nodes. do this, and remove the other interval from consideration
          interval = interval.union( other );
          this.layerChangeIntervals.splice( i--, 1 ); // decrement to stay at the same index
          sceneryLayerLog && sceneryLayerLog( 'removing interval: ' + other.toString() );
        }
      }
      
      this.layerChangeIntervals.push( interval );
      
      if ( sceneryLayerLog ) {
        sceneryLayerLog( 'new intervals: ' );
        _.each( this.layerChangeIntervals, function( interval ) {
          sceneryLayerLog( '  ' + interval.toString() );
        } );
        sceneryLayerLog( '---' );
      }
    },
    
    createLayer: function( layerType, layerArgs, startBoundary, endBoundary ) {
      var layer = layerType.createLayer( _.extend( {
        startBoundary: startBoundary,
        endBoundary: endBoundary
      }, layerArgs ) );
      layer.type = layerType;
      sceneryLayerLog && sceneryLayerLog( 'created layer: ' + layer.getId() + ' of type ' + layer.type.name );
      return layer;
    },
    
    // insert a layer into the proper place (from its starting boundary)
    insertLayer: function( layer ) {
      for ( var i = 0; i < this.layers.length; i++ ) {
        if ( layer.endPaintedTrail.isBefore( this.layers[i].startPaintedTrail ) ) {
          this.layers.splice( i, 0, layer ); // insert the layer here
          return;
        }
      }
      
      // it is after all other layers
      this.layers.push( layer );
    },
    
    getBoundaries: function() {
      // TODO: store these more efficiently!
      return [ this.layers[0].startBoundary ].concat( _.pluck( this.layers, 'endBoundary' ) );
    },
    
    calculateBoundaries: function( beforeLayerType, beforeTrail, afterTrail ) {
      sceneryLayerLog && sceneryLayerLog( 'build between ' + ( beforeTrail ? beforeTrail.toString() : beforeTrail ) + ',' + ( afterTrail ? afterTrail.toString() : afterTrail ) + ' with beforeType: ' + ( beforeLayerType ? beforeLayerType.name : null ) );
      var builder = new scenery.LayerBuilder( this, beforeLayerType, beforeTrail, afterTrail );
      
      // push the preferred layer type before we push that for any nodes
      if ( this.preferredSceneLayerType ) {
        builder.pushPreferredLayerType( this.preferredSceneLayerType );
      }
      
      builder.run();
      
      return builder.boundaries;
    },
    
    stitch: function( match ) {
      var scene = this;
      
      var i;
      
      sceneryLayerLog && sceneryLayerLog( '-----------------------------------\nbeginning stitch' );
      
      // bail out if there are no changes to stitch (stitch is called multiple times)
      if ( !this.layerChangeIntervals.length ) {
        return;
      }
      
      // data to be shared across all of the individually stitched intervals
      var stitchData = {
        // all instances that are affected, in no particular order (and may contain duplicates)
        affectedInstances: [],
        
        // fresh layers that should be added into the scene
        newLayers: []
      };
      
      // default arguments for constructing layers
      var layerArgs = {
        $main: this.$main,
        scene: this,
        baseNode: this
      };
      
      // reindex intervals, since their endpoints indices may need to be updated
      i = this.layerChangeIntervals.length;
      while ( i-- ) {
        this.layerChangeIntervals[i].reindex();
      }
      
      /*
       * Sort our intervals, so that when we need to 'unglue' a layer into two separate layers, we will have passed
       * all of the parts where we would need to use the 'before' layer, so we can update our layer map with the 'after'
       * layer.
       */
      this.layerChangeIntervals.sort( scenery.RenderInterval.compareDisjoint );
      
      sceneryLayerLog && sceneryLayerLog( 'stitching on intervals: \n' + this.layerChangeIntervals.join( '\n' ) );
      
      for ( i = 0; i < this.layerChangeIntervals.length; i++ ) {
        var interval = this.layerChangeIntervals[i];
        
        sceneryLayerLog && sceneryLayerLog( 'stitch on interval ' + interval.toString() );
        var beforeTrail = interval.start;
        var afterTrail = interval.end;
        
        var beforeInstance = beforeTrail ? beforeTrail.getInstance() : null;
        var afterInstance = afterTrail ? afterTrail.getInstance() : null;
        
        var beforeLayer = beforeInstance ? beforeInstance.layer : null;
        var afterLayer = afterInstance ? afterInstance.layer : null;
        
        // TODO: calculate boundaries based on the instances?
        var boundaries = this.calculateBoundaries( beforeLayer ? beforeLayer.type : null, beforeTrail, afterTrail );
        
        this.stitchInterval( stitchData, layerArgs, beforeTrail, afterTrail, beforeLayer, afterLayer, boundaries, match );
      }
      
      // clean up state that was set leading up to the stitching, and do it early so
      // if we do things later that cause side-effects we won't clear intervals that haven't been stitched
      this.layerChangeIntervals.length = 0;
      
      sceneryLayerLog && sceneryLayerLog( '------ finished intervals in stitching' );
      
      // reindex all of the relevant layer trails, and dispose/add as necessary
      i = this.layers.length;
      while ( i-- ) {
        var layer = this.layers[i];
        layer.startBoundary.reindex();
        layer.endBoundary.reindex(); // TODO: performance: this repeats some work, verify in layer audit that we are sharing boundaries properly, then only reindex end boundary on last layer
        
        // remove necessary layers. do this before adding layers, since insertLayer currently does not gracefully handle weird overlapping cases
        // layers with zero trails should be removed
        if ( layer._instanceCount === 0 ) {
          sceneryLayerLog && sceneryLayerLog( 'disposing layer: ' + layer.getId() );
          this.disposeLayer( layer );
        }
      }
      i = stitchData.newLayers.length;
      while ( i-- ) {
        var newLayer = stitchData.newLayers[i];
        newLayer.startBoundary.reindex();
        newLayer.endBoundary.reindex(); // TODO: performance: this repeats some work, verify in layer audit that we are sharing boundaries properly, then only reindex end boundary on last layer
        
        // add new layers. we do this before the add/remove trails, since those can trigger layer side effects
        sceneryAssert && sceneryAssert( newLayer._instanceCount, 'ensure we are not adding empty layers' );
        
        sceneryLayerLog && sceneryLayerLog( 'inserting layer: ' + newLayer.getId() );
        scene.insertLayer( newLayer );
      }
      
      // set the layers' elements' z-indices, and reindex their trails so they are in a consistent state
      // TODO: performance: don't reindex layers if no layers were added or removed?
      this.reindexLayers();
      
      sceneryLayerLog && sceneryLayerLog( '------ updating layer references' );
      
      // add/remove trails from their necessary layers
      var affectedLen = stitchData.affectedInstances.length;
      for ( i = 0; i < affectedLen; i++ ) {
        stitchData.affectedInstances[i].updateLayer();
      }
      
      sceneryAssertExtra && sceneryAssertExtra( this.layerAudit() );
      
      sceneryLayerLog && sceneryLayerLog( 'finished stitch\n-----------------------------------' );
    },
    
    /*
     * Stitching intervals has essentially two specific modes:
     * non-matching: handles added or removed nodes (and this can span multiple, even adjacent trails)
     * matching: handles in-place layer refreshes (no nodes removed or added, but something like a renderer was changed)
     *
     * This separation occurs since for matching, we want to match old layers with possible new layers, so we can keep trails in their
     * current layer instead of creating an identical layer and moving the trails to that layer.
     *
     * The stitching basically re-does the layering between a start and end trail, attempting to minimize the amount of changes made.
     * It can include 'gluing' layers together (a node that caused layer splits was removed, and before/after layers are joined),
     * 'ungluing' layers (an inserted node causes a layer split in an existing layer, and it is separated into a before/after),
     * or normal updating of the interior.
     *
     * The beforeTrail and afterTrail should be outside the modifications, and if the modifications are to the start/end of the graph,
     * they should be passed as null to indicate 'before everything' and 'after everything' respectively.
     *
     * Here be dragons!
     */
    stitchInterval: function( stitchData, layerArgs, beforeTrail, afterTrail, beforeLayer, afterLayer, boundaries, match ) {
      var scene = this;
      
      // make sure our beforeTrail and afterTrail are immutable
      beforeTrail && beforeTrail.setImmutable();
      afterTrail && afterTrail.setImmutable();
      
      // need a reference to this, since it may change
      var afterLayerEndBoundary = afterLayer ? afterLayer.endBoundary : null;
      
      var beforeLayerIndex = beforeLayer ? _.indexOf( this.layers, beforeLayer ) : -1;
      var afterLayerIndex = afterLayer ? _.indexOf( this.layers, afterLayer ) : this.layers.length;
      
      var beforePointer = beforeTrail ? new scenery.TrailPointer( beforeTrail, true ) : new scenery.TrailPointer( new scenery.Trail( this ), true );
      var afterPointer = afterTrail ? new scenery.TrailPointer( afterTrail, true ) : new scenery.TrailPointer( new scenery.Trail( this ), false );
      
      sceneryLayerLog && sceneryLayerLog( '\nstitching with boundaries:\n' + _.map( boundaries, function( boundary ) { return boundary.toString(); } ).join( '\n' ) );
      sceneryLayerLog && sceneryLayerLog( '               layers: ' + ( beforeLayer ? beforeLayer.getId() : '-' ) + ' to ' + ( afterLayer ? afterLayer.getId() : '-' ) );
      sceneryLayerLog && sceneryLayerLog( '               trails: ' + ( beforeTrail ? beforeTrail.toString() : '-' ) + ' to ' + ( afterTrail ? afterTrail.toString() : '-' ) );
      sceneryLayerLog && sceneryLayerLog( '               match: ' + match );
      
      /*---------------------------------------------------------------------------*
      * State
      *----------------------------------------------------------------------------*/
      
      var nextBoundaryIndex = 0;
      var nextBoundary = boundaries[nextBoundaryIndex];
      var instancesToAddToLayer = [];
      var currentTrail = beforeTrail;
      var currentLayer = beforeLayer;
      var currentLayerType = beforeLayer ? beforeLayer.type : null;
      var currentStartBoundary = null;
      var matchingLayer = null; // set whenever a trail has a matching layer, cleared after boundary
      
      function addPendingTrailsToLayer() {
        // add the necessary nodes to the layer
        var len = instancesToAddToLayer.length;
        for ( var i = 0; i < len; i++ ) {
          var instance = instancesToAddToLayer[i];
          instance.changeLayer( currentLayer );
          stitchData.affectedInstances.push( instance );
        }
        instancesToAddToLayer.length = 0;
      }
      
      function addAndCreateLayer( startBoundary, endBoundary ) {
        currentLayer = scene.createLayer( currentLayerType, layerArgs, startBoundary, endBoundary );
        stitchData.newLayers.push( currentLayer );
      }
      
      function step( trail, isEnd ) {
        sceneryLayerLog && sceneryLayerLog( 'step: ' + ( trail ? trail.toString() : trail ) );
        trail && trail.setImmutable(); // we don't want our trail to be modified, so we can store direct references to it
        // check for a boundary at this step between currentTrail and trail
        
        // if there is no next boundary, don't bother checking anyways
        if ( nextBoundary && nextBoundary.equivalentPreviousTrail( currentTrail ) ) { // at least one null check
          sceneryAssert && sceneryAssert( nextBoundary.equivalentNextTrail( trail ) );
          
          sceneryLayerLog && sceneryLayerLog( nextBoundary.toString() );
          
          // we are at a boundary change. verify that we are at the end of a layer
          if ( currentLayer || currentStartBoundary ) {
            if ( currentLayer ) {
              sceneryLayerLog && sceneryLayerLog( 'has currentLayer: ' + currentLayer.getId() );
              // existing layer, reposition its endpoint
              currentLayer.setEndBoundary( nextBoundary );
            } else {
              sceneryAssert && sceneryAssert( currentStartBoundary );
              
              if ( matchingLayer ) {
                sceneryLayerLog && sceneryLayerLog( 'matching layer used: ' + matchingLayer.getId() );
                matchingLayer.setStartBoundary( currentStartBoundary );
                matchingLayer.setEndBoundary( nextBoundary );
                currentLayer = matchingLayer;
              } else {
                sceneryLayerLog && sceneryLayerLog( 'creating layer' );
                addAndCreateLayer( currentStartBoundary, nextBoundary ); // sets currentLayer
              }
            }
            // sanity checks
            sceneryAssert && sceneryAssert( currentLayer.startPaintedTrail );
            sceneryAssert && sceneryAssert( currentLayer.endPaintedTrail );
            
            addPendingTrailsToLayer();
          } else {
            // if not at the end of a layer, sanity check that we should have no accumulated pending trails
            sceneryLayerLog && sceneryLayerLog( 'was first layer' );
            sceneryAssert && sceneryAssert( instancesToAddToLayer.length === 0 );
          }
          currentLayer = null;
          currentLayerType = nextBoundary.nextLayerType;
          currentStartBoundary = nextBoundary;
          matchingLayer = null;
          nextBoundaryIndex++;
          nextBoundary = boundaries[nextBoundaryIndex];
        }
        if ( trail && !isEnd ) {
          // TODO: performance: handle instances natively, don't just convert here
          instancesToAddToLayer.push( trail.getInstance() );
        }
        if ( match && !isEnd ) { // TODO: verify this condition with test cases
          // if the node's old layer is compatible
          // TODO: performance: don't use getInstance() here, use instances natively
          var layer = trail.getInstance().layer; // lookup should return the old layer from the system
          if ( layer.type === currentLayerType && !forceNewLayers ) {
            // TODO: we need to handle compatibility with layer splits. using forceNewLayers flag to temporarily disable
            matchingLayer = layer;
          }
        }
        currentTrail = trail;
      }
      
      function startStep( trail ) {
        sceneryLayerLog && sceneryLayerLog( 'startStep: ' + ( trail ? trail.toString() : trail ) );
      }
      
      function middleStep( trail ) {
        sceneryLayerLog && sceneryLayerLog( 'middleStep: ' + trail.toString() );
        step( trail, false );
      }
      
      function endStep( trail ) {
        sceneryLayerLog && sceneryLayerLog( 'endStep: ' + ( trail ? trail.toString() : trail ) );
        step( trail, true );
        
        if ( beforeLayer !== afterLayer && boundaries.length === 0 ) {
          // glue the layers together
          sceneryLayerLog && sceneryLayerLog( 'gluing layer' );
          sceneryLayerLog && sceneryLayerLog( 'endBoundary: ' + afterLayer.endBoundary.toString() );
          beforeLayer.setEndBoundary( afterLayer.endBoundary );
          currentLayer = beforeLayer;
          addPendingTrailsToLayer();
          
          // move over all of afterLayer's trails to beforeLayer
          var len = afterLayer._layerTrails.length;
          for ( var i = 0; i < len; i++ ) {
            var endTrail = afterLayer._layerTrails[i];
            
            endTrail.reindex();
            var instance = endTrail.getInstance();
            instance.changeLayer( beforeLayer ); // TODO: performance: handle instances natively
            stitchData.affectedInstances.push( instance );
          }
          
        } else if ( beforeLayer && beforeLayer === afterLayer && boundaries.length > 0 ) {
          // need to 'unglue' and split the layer
          sceneryLayerLog && sceneryLayerLog( 'ungluing layer' );
          sceneryAssert && sceneryAssert( currentStartBoundary );
          addAndCreateLayer( currentStartBoundary, afterLayerEndBoundary ); // sets currentLayer
          addPendingTrailsToLayer();
          
          currentLayer.endPaintedTrail.reindex(); // currentLayer's trails may be stale at this point
          scenery.Trail.eachPaintedTrailBetween( afterTrail, currentLayer.endPaintedTrail, function( subtrail ) {
            var instance = subtrail.getInstance();
            instance.changeLayer( currentLayer );
            stitchData.affectedInstances.push( instance );
          }, false, scene );
        } else if ( !beforeLayer && !afterLayer && boundaries.length === 1 && !boundaries[0].hasNext() && !boundaries[0].hasPrevious() ) {
          // TODO: why are we generating a boundary here?!?
        } else {
          currentLayer = afterLayer;
          // TODO: check concepts on this guard, since it seems sketchy
          if ( currentLayer && currentStartBoundary ) {
            currentLayer.setStartBoundary( currentStartBoundary );
          }
          
          addPendingTrailsToLayer();
        }
      }
      
      // iterate from beforeTrail up to BEFORE the afterTrail. does not include afterTrail
      startStep( beforeTrail );
      beforePointer.eachTrailBetween( afterPointer, function( trail ) {
        // ignore non-self trails
        if ( !trail.isPainted() || ( beforeTrail && trail.equals( beforeTrail ) ) ) {
          return;
        }
        
        middleStep( trail.copy() );
      } );
      endStep( afterTrail );
    },
    
    rebuildLayers: function() {
      sceneryLayerLog && sceneryLayerLog( 'Scene: rebuildLayers' );
      
      // mark the entire scene 
      this.markInterval( new scenery.Trail( this ) );
      
      // then stitch with match=true
      this.stitch( true );
    },
    
    // after layer changes, the layers should have their zIndex updated, and updates their trails
    reindexLayers: function() {
      sceneryLayerLog && sceneryLayerLog( 'Scene: reindexLayers' );
      
      var index = 1; // don't start below 1
      if ( accessibility && this.accessibiltyLayer ) {
        this.accessibilityLayer.style.zIndex = 9999; // TODO: a better way than 9999, SR says probably unnecessary
        index++;
      }
      
      var len = this.layers.length;
      for ( var i = 0; i < len; i++ ) {
        index = this.layers[i].reindex( index );
      }
      
      if ( accessibility ) {
        if ( this.focusRingSVGContainer ) {
          this.focusRingSVGContainer.style.zIndex = index++;
        }
      }

      if (this.pointerOverlay){
        this.pointerOverlay.setZIndex( index++ );
      }
    },
    
    dispose: function() {
      this.disposeLayers();
      if ( this.input ) {
        this.input.disposeListeners();
      }
      
      // remove self reference from the container
      delete this.main.scene;
      
      // TODO: clear event handlers if added
      //throw new Error( 'unimplemented dispose: clear event handlers if added' );
    },
    
    disposeLayer: function( layer ) {
      // NOTE: stitching relies on this not changing this.layers except for removing the specific layer
      layer.dispose();
      this.layers.splice( _.indexOf( this.layers, layer ), 1 ); // TODO: better removal code!
    },
    
    disposeLayers: function() {
      var scene = this;
      
      var i = this.layers.length;
      while ( i-- ) {
        this.disposeLayer( this.layers[i] );
      }
    },
    
    // all layers whose start or end points lie inclusively in the range from the trail's before and after
    affectedLayers: function( trail ) {
      // midpoint search and result depends on the order of layers being in render order (bottom to top)
      
      sceneryAssert && sceneryAssert( !( trail.isEmpty() || trail.nodes[0] !== this ), 'layerLookup root matches' );
      
      var n = this.layers.length;
      if ( n === 0 ) {
        sceneryAssert && sceneryAssert( !trail.lastNode().isPainted(), 'There should be at least one layer for a painted trail' );
        return [];
      }
      
      sceneryAssert && sceneryAssert( trail.areIndicesValid() );
      
      var layers = this.layers;
      
      // from layers 0 to n-1, notAfter goes from false to true, notBefore goes from true to false
      var low = -1;
      var high = n;
      var mid;
      
      // midpoint search to see where our trail's start isn't after a layer's end
      while ( high - 1 > low ) {
        mid = ( high + low ) >> 1;
        var endTrail = layers[mid].endPaintedTrail;
        sceneryAssert && sceneryAssert( endTrail.areIndicesValid() );
        // NOTE TO SELF: don't change this flag to true again. think it through
        // trail,true points to the beginning of the node, right before it would be rendered
        var notAfter = scenery.TrailPointer.compareNested( trail, true, endTrail, true ) !== 1;
        if ( notAfter ) {
          high = mid;
        } else {
          low = mid;
        }
      }
      
      // store result and reset bound
      var firstIndex = high;
      low = -1;
      high = n;
      
      // midpoint search to see where our trail's end isn't before a layer's start
      while ( high - 1 > low ) {
        mid = ( high + low ) >> 1;
        var startTrail = layers[mid].startPaintedTrail;
        startTrail.reindex();
        sceneryAssert && sceneryAssert( startTrail.areIndicesValid() );
        var notBefore = scenery.TrailPointer.compareNested( trail, false, startTrail, true ) !== -1;
        if ( notBefore ) {
          low = mid;
        } else {
          high = mid;
        }
      }
      
      var lastIndex = low;
      
      return layers.slice( firstIndex, lastIndex + 1 );
    },
    
    // attempt to render everything currently visible in the scene to an external canvas. allows copying from canvas layers straight to the other canvas
    renderToCanvas: function( canvas, context, callback ) {
      var count = 0;
      var started = false; // flag guards against asynchronous tests that call back synchronously (immediate increment and decrement)
      var delayCounts = {
        increment: function() {
          count++;
        },
        
        decrement: function() {
          count--;
          if ( count === 0 && callback && started ) {
            callback();
          }
        }
      };
      
      context.clearRect( 0, 0, canvas.width, canvas.height );
      var len = this.layers.length;
      for ( var i = 0; i < len; i++ ) {
        this.layers[i].renderToCanvas( canvas, context, delayCounts );
      }
      
      if ( count === 0 ) {
        // no asynchronous layers, callback immediately
        if ( callback ) {
          callback();
        }
      } else {
        started = true;
      }
    },
    
    // TODO: consider SVG data URLs
    canvasDataURL: function( callback ) {
      this.canvasSnapshot( function( canvas ) {
        callback( canvas.toDataURL() );
      } );
    },
    
    // renders what it can into a Canvas (so far, Canvas and SVG layers work fine)
    canvasSnapshot: function( callback ) {
      var canvas = document.createElement( 'canvas' );
      canvas.width = this.sceneBounds.getWidth();
      canvas.height = this.sceneBounds.getHeight();
      
      var context = canvas.getContext( '2d' );
      this.renderToCanvas( canvas, context, function() {
        callback( canvas, context.getImageData( 0, 0, canvas.width, canvas.height ) );
      } );
    },
    
    // TODO: Note that this is private, better name?
    setSize: function( width, height ) {
      // resize our main container
      this.$main.width( width );
      this.$main.height( height );
      
      // set the container's clipping so anything outside won't show up
      // TODO: verify this clipping doesn't reduce performance!
      this.$main.css( 'clip', 'rect(0px,' + width + 'px,' + height + 'px,0px)' );
      
      this.sceneBounds = new Bounds2( 0, 0, width, height );
    },
    
    resize: function( width, height ) {
      if ( this.sceneBounds.width !== width || this.sceneBounds.height !== height ) {
        this.setSize( width, height );
        this.rebuildLayers(); // TODO: why? - change this to resize individual layers

        if ( accessibility ) {
          this.resizeAccessibilityLayer( width, height );
          this.resizeFocusRingSVGContainer( width, height );
          
          //Update the focus ring when the scene resizes.  Note: as of 5/10/2013 this only works properly when scaling up, and is buggy (off by a translation) when scaling down
          if ( this.updateFocusRing && this.activePeer) {
            // this.updateScene();
            this.updateFocusRing();
          }
        }
        
        if ( this.input ) {
          this.input.validatePointers();
        }
        
        this.fireEvent( 'resize', { width: width, height: height } );
      }
    },

    // TODO: Refactor the following methods into one to avoid code duplication.
    resizeAccessibilityLayer: function( width, height ) {
      if ( this.accessibilityLayer ) {
        this.accessibilityLayer.setAttribute( 'width', width );
        this.accessibilityLayer.setAttribute( 'height', height );
        this.accessibilityLayer.style.clip = 'rect(0px,' + width + 'px,' + height + 'px,0px)';
      }
    },
    
    resizeFocusRingSVGContainer: function( width, height ) {
      if ( this.focusRingSVGContainer ) {
        this.focusRingSVGContainer.setAttribute( 'width', width );
        this.focusRingSVGContainer.setAttribute( 'height', height );
        this.focusRingSVGContainer.style.clip = 'rect(0px,' + width + 'px,' + height + 'px,0px)';
      }
    },

    getSceneWidth: function() {
      return this.sceneBounds.getWidth();
    },
    
    getSceneHeight: function() {
      return this.sceneBounds.getHeight();
    },
    
    markSceneForLayerRefresh: function( instance ) {
      sceneryLayerLog && sceneryLayerLog( 'Scene: marking layer refresh: ' + instance.trail.toString() );
      this.markInterval( instance.trail );
    },
    
    markSceneForInsertion: function( instance, child, index ) {
      var affectedTrail = instance.trail.copy().addDescendant( child );
      sceneryLayerLog && sceneryLayerLog( 'Scene: marking insertion: ' + affectedTrail.toString() );
      
      sceneryLayerLog && sceneryLayerLog( 'inserting instances onto ' + instance.toString() + ' with child ' + child.id + ' and index ' + index );
      var baseInstance = instance.createChild( child, index );
      
      buildInstances( baseInstance );
      
      this.markInterval( affectedTrail );
    },
    
    markSceneForRemoval: function( instance, child, index ) {
      // mark the interval
      var affectedTrail = instance.trail.copy().addDescendant( child );
      sceneryLayerLog && sceneryLayerLog( 'Scene: marking removal: ' + affectedTrail.toString() );
      this.markInterval( affectedTrail );
      
      // remove the necessary instances
      var toRemove = [ instance.children[index] ];
      instance.removeInstance( index );
      while ( toRemove.length ) {
        var item = toRemove.pop();
        sceneryAssert && sceneryAssert( item, 'item instance should always exist' );
        
        // add its children
        Array.prototype.push.apply( toRemove, item.children );
        
        item.dispose(); // removes it from the node and sets it up for easy GC
      }
    },
    
    updateCursor: function() {
      if ( this.input && this.input.mouse && this.input.mouse.point ) {
        var mouseTrail = this.trailUnderPoint( this.input.mouse.point, { isMouse: true } );
        
        if ( mouseTrail ) {
          for ( var i = mouseTrail.length - 1; i >= 0; i-- ) {
            var cursor = mouseTrail.nodes[i].getCursor();
            
            if ( cursor ) {
              this.setSceneCursor( cursor );
              return;
            }
          }
        }
      }
      
      // fallback case
      this.setSceneCursor( this.defaultCursor );
    },
    
    setSceneCursor: function( cursor ) {
      if ( cursor !== this.lastCursor ) {
        this.lastCursor = cursor;
        this.$main.css( 'cursor', cursor );
      }
    },
    
    updateOnRequestAnimationFrame: function( element ) {
      var scene = this;
      (function step() {
        window.requestAnimationFrame( step, element );
        scene.updateScene();
      })();
    },
    
    initializeStandaloneEvents: function( parameters ) {
      // TODO extract similarity between standalone and fullscreen!
      var element = this.$main[0];
      this.initializeEvents( _.extend( {}, {
        listenerTarget: element,
        pointFromEvent: function pointFromEvent( evt ) {
          var mainBounds = element.getBoundingClientRect();
          return Vector2.createFromPool( evt.clientX - mainBounds.left, evt.clientY - mainBounds.top );
        }
      }, parameters ) );
    },
    
    initializeFullscreenEvents: function( parameters ) {
      var element = this.$main[0];
      this.initializeEvents( _.extend( {}, {
        listenerTarget: document,
        pointFromEvent: function pointFromEvent( evt ) {
          var mainBounds = element.getBoundingClientRect();
          return Vector2.createFromPool( evt.clientX - mainBounds.left, evt.clientY - mainBounds.top );
        }
      }, parameters ) );
    },
    
    initializeWindowEvents: function( parameters ) {
      var element = this.$main[0];
      this.initializeEvents( _.extend( {}, {
        listenerTarget: window,
        pointFromEvent: function pointFromEvent( evt ) {
          var mainBounds = element.getBoundingClientRect();
          return Vector2.createFromPool( evt.clientX - mainBounds.left, evt.clientY - mainBounds.top );
        }
      }, parameters ) );
    },
    
    initializeEvents: function( parameters ) {
      var scene = this;
      
      if ( scene.input ) {
        throw new Error( 'Attempt to attach events twice to the scene' );
      }
      
      // TODO: come up with more parameter names that have the same string length, so it looks creepier
      var pointFromEvent = parameters.pointFromEvent;
      var listenerTarget = parameters.listenerTarget;
      var batchDOMEvents = parameters.batchDOMEvents;
      
      var input = new scenery.Input( scene, listenerTarget, !!batchDOMEvents );
      scene.input = input;
      
      // maps the current MS pointer types onto the pointer spec
      function msPointerType( evt ) {
        if ( evt.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_TOUCH ) {
          return 'touch';
        } else if ( evt.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_PEN ) {
          return 'pen';
        } else if ( evt.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_MOUSE ) {
          return 'mouse';
        } else {
          return evt.pointerType; // hope for the best
        }
      }
      
      function forEachChangedTouch( evt, callback ) {
        for ( var i = 0; i < evt.changedTouches.length; i++ ) {
          // according to spec (http://www.w3.org/TR/touch-events/), this is not an Array, but a TouchList
          var touch = evt.changedTouches.item( i );
          
          callback( touch.identifier, pointFromEvent( touch ) );
        }
      }
      
      // TODO: massive boilerplate reduction! closures should help tons!
      
      var implementsPointerEvents = window.navigator && window.navigator.pointerEnabled; // W3C spec for pointer events
      var implementsMSPointerEvents = window.navigator && window.navigator.msPointerEnabled; // MS spec for pointer event
      if ( this.enablePointerEvents && implementsPointerEvents ) {
        sceneryEventLog && sceneryEventLog( 'Detected pointer events support, using that instead of mouse/touch events' );
        // accepts pointer events corresponding to the spec at http://www.w3.org/TR/pointerevents/
        input.addListener( 'pointerdown', function pointerDownCallback( domEvent ) {
          input.pointerDown( domEvent.pointerId, domEvent.pointerType, pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'pointerup', function pointerUpCallback( domEvent ) {
          input.pointerUp( domEvent.pointerId, domEvent.pointerType, pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'pointermove', function pointerMoveCallback( domEvent ) {
          input.pointerMove( domEvent.pointerId, domEvent.pointerType, pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'pointerover', function pointerOverCallback( domEvent ) {
          input.pointerOver( domEvent.pointerId, domEvent.pointerType, pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'pointerout', function pointerOutCallback( domEvent ) {
          input.pointerOut( domEvent.pointerId, domEvent.pointerType, pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'pointercancel', function pointerCancelCallback( domEvent ) {
          input.pointerCancel( domEvent.pointerId, domEvent.pointerType, pointFromEvent( domEvent ), domEvent );
        } );
        // immediate version
        input.addImmediateListener( 'pointerup', function pointerUpCallback( domEvent ) {
          input.pointerUpImmediate( domEvent.pointerId, domEvent.pointerType, pointFromEvent( domEvent ), domEvent );
        } );
      } else if ( this.enablePointerEvents && implementsMSPointerEvents ) {
        sceneryEventLog && sceneryEventLog( 'Detected MS pointer events support, using that instead of mouse/touch events' );
        input.addListener( 'MSPointerDown', function msPointerDownCallback( domEvent ) {
          input.pointerDown( domEvent.pointerId, msPointerType( domEvent ), pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'MSPointerUp', function msPointerUpCallback( domEvent ) {
          input.pointerUp( domEvent.pointerId, msPointerType( domEvent ), pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'MSPointerMove', function msPointerMoveCallback( domEvent ) {
          input.pointerMove( domEvent.pointerId, msPointerType( domEvent ), pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'MSPointerOver', function msPointerOverCallback( domEvent ) {
          input.pointerOver( domEvent.pointerId, msPointerType( domEvent ), pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'MSPointerOut', function msPointerOutCallback( domEvent ) {
          input.pointerOut( domEvent.pointerId, msPointerType( domEvent ), pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'MSPointerCancel', function msPointerCancelCallback( domEvent ) {
          input.pointerCancel( domEvent.pointerId, msPointerType( domEvent ), pointFromEvent( domEvent ), domEvent );
        } );
        // immediate version
        input.addImmediateListener( 'MSPointerUp', function msPointerUpCallback( domEvent ) {
          input.pointerUpImmediate( domEvent.pointerId, msPointerType( domEvent ), pointFromEvent( domEvent ), domEvent );
        } );
      } else {
        sceneryEventLog && sceneryEventLog( 'No pointer events support detected, using mouse/touch events' );
        input.addListener( 'mousedown', function mouseDownCallback( domEvent ) {
          input.mouseDown( pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'mouseup', function mouseUpCallback( domEvent ) {
          input.mouseUp( pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'mousemove', function mouseMoveCallback( domEvent ) {
          input.mouseMove( pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'mouseover', function mouseOverCallback( domEvent ) {
          input.mouseOver( pointFromEvent( domEvent ), domEvent );
        } );
        input.addListener( 'mouseout', function mouseOutCallback( domEvent ) {
          input.mouseOut( pointFromEvent( domEvent ), domEvent );
        } );
        // immediate version
        input.addImmediateListener( 'mouseup', function mouseUpCallback( domEvent ) {
          input.mouseUpImmediate( pointFromEvent( domEvent ), domEvent );
        } );
        
        input.addListener( 'touchstart', function touchStartCallback( domEvent ) {
          sceneryEventLog && sceneryEventLog( 'touchstart (multiple events)' );
          forEachChangedTouch( domEvent, function touchStartTouch( id, point ) {
            input.touchStart( id, point, domEvent );
          } );
        } );
        input.addListener( 'touchend', function touchEndCallback( domEvent ) {
          sceneryEventLog && sceneryEventLog( 'touchend (multiple events)' );
          forEachChangedTouch( domEvent, function touchEndTouch( id, point ) {
            input.touchEnd( id, point, domEvent );
          } );
        } );
        input.addListener( 'touchmove', function touchMoveCallback( domEvent ) {
          sceneryEventLog && sceneryEventLog( 'touchmove (multiple events)' );
          forEachChangedTouch( domEvent, function touchMoveTouch( id, point ) {
            input.touchMove( id, point, domEvent );
          } );
        } );
        input.addListener( 'touchcancel', function touchCancelCallback( domEvent ) {
          sceneryEventLog && sceneryEventLog( 'touchcancel (multiple events)' );
          forEachChangedTouch( domEvent, function touchCancelTouch( id, point ) {
            input.touchCancel( id, point, domEvent );
          } );
        } );
        // immediate version
        input.addImmediateListener( 'touchend', function touchEndCallback( domEvent ) {
          sceneryEventLog && sceneryEventLog( 'touchend immediate (multiple events)' );
          forEachChangedTouch( domEvent, function touchEndTouch( id, point ) {
            input.touchEndImmediate( id, point, domEvent );
          } );
        } );
      }
      
      input.addListener( 'keyup', function keyUpCallback( domEvent ) {
        input.keyUp( domEvent );
      } );
      input.addListener( 'keydown', function keyDownCallback( domEvent ) {
        input.keyDown( domEvent );
      } );
      input.addListener( 'keypress', function keyPressCallback( domEvent ) {
        input.keyPress( domEvent );
      } );
    },

    setPointerDisplayVisible: function( visible ) {
      if ( visible && !this.pointerOverlay ) {
        this.pointerOverlay = new scenery.PointerOverlay( this );
      }
      else if ( !visible && this.pointerOverlay ) {
        this.pointerOverlay.dispose();
        delete this.pointerOverlay;
      }
    },

    getTrailFromKeyboardFocus: function() {
      // return the root (scene) trail by default
      // TODO: fill in with actual keyboard focus
      return new scenery.Trail( this );
    },
    
    fireBatchedEvents: function() {
      this.input.fireBatchedEvents();
    },
    
    resizeOnWindowResize: function() {
      var scene = this;
      
      var resizer = function() {
        scene.resize( window.innerWidth, window.innerHeight );
      };
      $( window ).resize( resizer );
      resizer();
    },
    
    // in-depth check to make sure everything is layered properly
    layerAudit: function() {
      var scene = this;
      
      var boundaries = this.calculateBoundaries( null, null, null );
      sceneryAssert && sceneryAssert( boundaries.length === this.layers.length + 1, 'boundary count (' + boundaries.length + ') does not match layer count (' + this.layers.length + ') + 1' );
      
      // count how many 'self' trails there are
      var eachTrailUnderPaintedCount = 0;
      new scenery.Trail( this ).eachTrailUnder( function( trail ) {
        if ( trail.isPainted() ) {
          eachTrailUnderPaintedCount++;
          
          sceneryAssert && sceneryAssert( trail.getInstance(), 'every painted trail must have an instance' );
        }
        
        sceneryAssert && sceneryAssert( trail.getInstance() && trail.getInstance().trail.equals( trail ), 'every trail must have a single corresponding instance' );
      } );
      
      var layerPaintedCount = 0;
      _.each( this.layers, function( layer ) {
        layerPaintedCount += layer.getLayerTrails().length;
        
        // reindex now so we don't have problems later
        layer.startPaintedTrail.reindex();
        layer.endPaintedTrail.reindex();
      } );
      
      var layerIterationPaintedCount = 0;
      _.each( this.layers, function( layer ) {
        var selfCount = 0;
        scenery.Trail.eachPaintedTrailBetween( layer.startPaintedTrail, layer.endPaintedTrail, function( trail ) {
          selfCount++;
        }, false, scene );
        sceneryAssert && sceneryAssert( selfCount > 0, 'every layer must have at least one self trail' );
        layerIterationPaintedCount += selfCount;
      } );
      
      sceneryAssert && sceneryAssert( eachTrailUnderPaintedCount === layerPaintedCount, 'cross-referencing self trail counts: layerPaintedCount, ' + eachTrailUnderPaintedCount + ' vs ' + layerPaintedCount );
      sceneryAssert && sceneryAssert( eachTrailUnderPaintedCount === layerIterationPaintedCount, 'cross-referencing self trail counts: layerIterationPaintedCount, ' + eachTrailUnderPaintedCount + ' vs ' + layerIterationPaintedCount );
      
      _.each( this.layers, function( layer ) {
        sceneryAssert && sceneryAssert( layer.startPaintedTrail.compare( layer.endPaintedTrail ) <= 0, 'proper ordering on layer trails' );
      } );
      
      for ( var i = 1; i < this.layers.length; i++ ) {
        sceneryAssert && sceneryAssert( this.layers[i-1].endPaintedTrail.compare( this.layers[i].startPaintedTrail ) === -1, 'proper ordering of layer trail boundaries in scene.layers array' );
        sceneryAssert && sceneryAssert( this.layers[i-1].endBoundary === this.layers[i].startBoundary, 'proper sharing of boundaries' );
      }
      
      _.each( this.layers, function( layer ) {
        // a list of trails that the layer tracks
        var layerTrails = layer.getLayerTrails();
        
        // a list of trails that the layer should be tracking (between painted trails)
        var computedTrails = [];
        scenery.Trail.eachPaintedTrailBetween( layer.startPaintedTrail, layer.endPaintedTrail, function( trail ) {
          computedTrails.push( trail.copy() );
        }, false, scene );
        
        // verify that the layer has an identical record of trails compared to the trails inside its boundaries
        sceneryAssert && sceneryAssert( layerTrails.length === computedTrails.length, 'layer has incorrect number of tracked trails' );
        _.each( layerTrails, function( trail ) {
          sceneryAssert && sceneryAssert( _.some( computedTrails, function( otherTrail ) { return trail.equals( otherTrail ); } ), 'layer has a tracked trail discrepancy' );
        } );
        
        // verify that each trail has the same (or null) renderer as the layer
        scenery.Trail.eachTrailBetween( layer.startPaintedTrail, layer.endPaintedTrail, function( trail ) {
          var node = trail.lastNode();
          sceneryAssert && sceneryAssert( !node.renderer || node.renderer.name === layer.type.name, 'specified renderers should match the layer renderer' );
        }, false, scene );
      } );
      
      // verify layer splits
      new scenery.Trail( this ).eachTrailUnder( function( trail ) {
        var beforeSplitTrail;
        var afterSplitTrail;
        if ( trail.lastNode().layerSplitBefore ) {
          beforeSplitTrail = trail.previousPainted();
          afterSplitTrail = trail.lastNode().isPainted() ? trail : trail.nextPainted();
          sceneryAssert && sceneryAssert( !beforeSplitTrail || !afterSplitTrail || beforeSplitTrail.getInstance().layer !== afterSplitTrail.getInstance().layer, 'layerSplitBefore layers need to be different' );
        }
        if ( trail.lastNode().layerSplitAfter ) {
          // shift a pointer from the (nested) end of the trail to the next isBefore (if available)
          var ptr = new scenery.TrailPointer( trail.copy(), false );
          while ( ptr && ptr.isAfter ) {
            ptr = ptr.nestedForwards();
          }
          
          // if !ptr, we walked off the end of the graph (nothing after layer split, automatically ok)
          if ( ptr ) {
            beforeSplitTrail = ptr.trail.previousPainted();
            afterSplitTrail = ptr.trail.lastNode().isPainted() ? ptr.trail : ptr.trail.nextPainted();
            sceneryAssert && sceneryAssert( !beforeSplitTrail || !afterSplitTrail || beforeSplitTrail.getInstance().layer !== afterSplitTrail.getInstance().layer, 'layerSplitAfter layers need to be different' );
          }
        }
      } );
      
      return true; // so we can assert( layerAudit() )
    },
    
    getDebugHTML: function() {
      var startPointer = new scenery.TrailPointer( new scenery.Trail( this ), true );
      var endPointer = new scenery.TrailPointer( new scenery.Trail( this ), false );
      
      function str( ob ) {
        return ob ? ob.toString() : ob;
      }
      
      var depth = 0;
      
      var result = '';
      
      var layerStartEntries = {};
      var layerEndEntries = {};
      _.each( this.layers, function( layer ) {
        var startIdx = layer.startPaintedTrail.getUniqueId();
        var endIndex = layer.endPaintedTrail.getUniqueId();
        layerStartEntries[startIdx] = '';
        layerEndEntries[endIndex] = '';
        layer.startPaintedTrail.reindex();
        layer.endPaintedTrail.reindex();
        var layerInfo = layer.getId() + ' <strong>' + layer.type.name + '</strong>' +
                        ' trails: ' + ( layer.startPaintedTrail ? str( layer.startPaintedTrail ) : layer.startPaintedTrail ) +
                        ',' + ( layer.endPaintedTrail ? str( layer.endPaintedTrail ) : layer.endPaintedTrail );
        layerInfo += '<span style="color: #008">';
        if ( layer.canUseDirtyRegions && !layer.canUseDirtyRegions() ) { layerInfo += ' dirtyRegionsDisabled'; }
        if ( layer.cssTranslation ) { layerInfo += ' cssTranslation'; }
        if ( layer.cssRotation ) { layerInfo += ' cssTranslation'; }
        if ( layer.cssScale ) { layerInfo += ' cssTranslation'; }
        if ( layer.cssTransform ) { layerInfo += ' cssTranslation'; }
        if ( layer.dirtyBounds && layer.dirtyBounds.isFinite() ) { layerInfo += ' dirtyBounds:' + layer.dirtyBounds.toString(); }
        layerInfo += '</span>';
        layerStartEntries[startIdx] += '<div style="color: #080">+Layer ' + layerInfo + '</div>';
        layerEndEntries[endIndex] += '<div style="color: #800">-Layer ' + layerInfo + '</div>';
      } );
      
      startPointer.depthFirstUntil( endPointer, function( pointer ) {
        var div;
        var ptr = str( pointer );
        var node = pointer.trail.lastNode();
        
        function addQualifier( text ) {
            div += ' <span style="color: #008">' + text + '</span>';
          }
        
        if ( pointer.isBefore && layerStartEntries[pointer.trail.getUniqueId()] ) {
          result += layerStartEntries[pointer.trail.getUniqueId()];
        }
        if ( pointer.isBefore ) {
          div = '<div style="margin-left: ' + ( depth * 20 ) + 'px">';
          if ( node.constructor.name ) {
            div += ' ' + node.constructor.name; // see http://stackoverflow.com/questions/332422/how-do-i-get-the-name-of-an-objects-type-in-javascript
          }
          div += ' <span style="font-weight: ' + ( node.isPainted() ? 'bold' : 'normal' ) + '">' + pointer.trail.lastNode().getId() + '</span>';
          div += ' <span style="color: #888">' + str( pointer.trail ) + '</span>';
          if ( !node._visible ) {
            addQualifier( 'invisible' );
          }
          if ( !node._pickable ) {
            addQualifier( 'unpickable' );
          }
          if ( node._clipShape ) {
            addQualifier( 'clipShape' );
          }
          if ( node._renderer ) {
            addQualifier( 'renderer:' + node._renderer.name );
          }
          if ( node._rendererOptions ) {
            // addQualifier( 'rendererOptions:' + _.each( node._rendererOptions, function( option, key ) { return key + ':' + str( option ); } ).join( ',' ) );
          }
          if ( node._layerSplitBefore ) {
            addQualifier( 'layerSplitBefore' );
          }
          if ( node._layerSplitAfter ) {
            addQualifier( 'layerSplitAfter' );
          }
          if ( node._opacity < 1 ) {
            addQualifier( 'opacity:' + node._opacity );
          }
          
          var transformType = '';
          switch ( node.transform.getMatrix().type ) {
            case Matrix3.Types.IDENTITY:       transformType = '';           break;
            case Matrix3.Types.TRANSLATION_2D: transformType = 'translated'; break;
            case Matrix3.Types.SCALING:        transformType = 'scale';      break;
            case Matrix3.Types.AFFINE:         transformType = 'affine';     break;
            case Matrix3.Types.OTHER:          transformType = 'other';      break;
          }
          if ( transformType ) {
            div += ' <span style="color: #88f" title="' + node.transform.getMatrix().toString().replace( '\n', '&#10;' ) + '">' + transformType + '</span>';
          }
          div += '</div>';
          result += div;
        }
        if ( pointer.isAfter && layerEndEntries[pointer.trail.getUniqueId()] ) {
          result += layerEndEntries[pointer.trail.getUniqueId()];
        }
        depth += pointer.isBefore ? 1 : -1;
      }, false );
      
      return result;
    },
    
    popupDebug: function() {
      var htmlContent = '<!DOCTYPE html>' +
                        '<html lang="en">' +
                        '<head><title>Scenery Debug Snapshot</title></head>' +
                        '<body>' + this.getDebugHTML() + '</body>' +
                        '</html>';
      window.open( 'data:text/html;charset=utf-8,' + encodeURIComponent( htmlContent ) );
    },
    
    getBasicConstructor: function( propLines ) {
      return 'new scenery.Scene( $( \'#main\' ), {' + propLines + '} )';
    },
    
    toStringWithChildren: function( mutateScene ) {
      var scene = this;
      var result = '';
      
      var nodes = this.getTopologicallySortedNodes().slice( 0 ).reverse(); // defensive slice, in case we store the order somewhere
      
      function name( node ) {
        return node === scene ? 'scene' : node.constructor.name.toLowerCase() + node.id;
      }
      
      _.each( nodes, function( node ) {
        if ( result ) {
          result += '\n';
        }
        
        if ( mutateScene && node === scene ) {
          var props = scene.getPropString( '  ', false );
          result += 'scene.mutate( {' + ( props ? ( '\n' + props + '\n' ) : '' ) + '} )';
        } else {
          result += 'var ' + name( node ) + ' = ' + node.toString( '', false );
        }
        
        _.each( node.children, function( child ) {
          result += '\n' + name( node ) + '.addChild( ' + name( child ) + ' );';
        } );
      } );
      
      return result;
    }
  } );
  
  function applyCSSHacks( $main, options ) {
    // to use CSS3 transforms for performance, hide anything outside our bounds by default
    if ( !options.allowSceneOverflow ) {
      $main.css( 'overflow', 'hidden' );
    }
    
    // forward all pointer events
    $main.css( '-ms-touch-action', 'none' );
    
    if ( options.allowCSSHacks ) {
      // some css hacks (inspired from https://github.com/EightMedia/hammer.js/blob/master/hammer.js)
      (function() {
        var prefixes = [ '-webkit-', '-moz-', '-ms-', '-o-', '' ];
        var properties = {
          userSelect: 'none',
          touchCallout: 'none',
          touchAction: 'none',
          userDrag: 'none',
          tapHighlightColor: 'rgba(0,0,0,0)'
        };
        
        _.each( prefixes, function( prefix ) {
          _.each( properties, function( propertyValue, propertyName ) {
            $main.css( prefix + propertyName, propertyValue );
          } );
        } );
      })();
    }
  }
  
  return Scene;
} );

// Copyright 2002-2013, University of Colorado

/**
 * A rectangular node that inherits Path, and allows for optimized drawing,
 * and improved rectangle handling.
 *
 * TODO: add DOM support
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/Rectangle',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/nodes/Path','KITE/Shape','DOT/Vector2','DOT/Bounds2'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );
  
  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );
  var Vector2 = require( 'DOT/Vector2' );
  var Bounds2 = require( 'DOT/Bounds2' );
  
  /**
   * Currently, all numerical parameters should be finite.
   * x:         x-position of the upper-left corner (left bound)
   * y:         y-position of the upper-left corner (top bound)
   * width:     width of the rectangle to the right of the upper-left corner, required to be >= 0
   * height:    height of the rectangle below the upper-left corner, required to be >= 0
   * arcWidth:  positive width of the rounded corner, or 0 to indicate the corner should be sharp
   * arcHeight: positive height of the rounded corner, or 0 to indicate the corner should be sharp
   */
  scenery.Rectangle = function Rectangle( x, y, width, height, arcWidth, arcHeight, options ) {
    if ( typeof x === 'object' ) {
      if ( x instanceof Bounds2 ) {
        // allow new Rectangle( bounds2, { ... } ) or new Rectangle( bounds2, arcWidth, arcHeight, options )
        this._rectX = x.minX;
        this._rectY = x.minY;
        this._rectWidth = x.width;
        this._rectHeight = x.height;
        if ( arguments.length < 3 ) {
          // Rectangle( bounds2, { ... } )
          options = y;
          this._rectArcWidth = 0;
          this._rectArcHeight = 0;
        } else {
          // Rectangle( bounds2, arcWidth, arcHeight, { ... } )
          options = height;
          this._rectArcWidth = y;
          this._rectArcHeight = width;
        }
      } else {
        // allow new Rectangle( { rectX: x, rectY: y, rectWidth: width, rectHeight: height, ... } )
        // the mutators will call invalidateRectangle() and properly set the shape
        options = x;
        this._rectX = options.rectX || 0;
        this._rectY = options.rectY || 0;
        this._rectWidth = options.rectWidth;
        this._rectHeight = options.rectHeight;
        this._rectArcWidth = options.rectArcWidth || 0;
        this._rectArcHeight = options.rectArcHeight || 0;
      }
    } else if ( arguments.length < 6 ) {
      // new Rectangle( x, y, width, height, [options] )
      this._rectX = x;
      this._rectY = y;
      this._rectWidth = width;
      this._rectHeight = height;
      this._rectArcWidth = 0;
      this._rectArcHeight = 0;
      
      // ensure we have a parameter object
      options = arcWidth || {};
      
    } else {
      // normal case with args (including arcWidth / arcHeight)
      this._rectX = x;
      this._rectY = y;
      this._rectWidth = width;
      this._rectHeight = height;
      this._rectArcWidth = arcWidth;
      this._rectArcHeight = arcHeight;
      
      // ensure we have a parameter object
      options = options || {};
      
    }
    // fallback for non-canvas or non-svg rendering, and for proper bounds computation

    Path.call( this, this.createRectangleShape(), options );
  };
  var Rectangle = scenery.Rectangle;
  
  inherit( Path, Rectangle, {
    setRect: function( x, y, width, height, arcWidth, arcHeight ) {
      sceneryAssert && sceneryAssert( x !== undefined && y !== undefined && width !== undefined && height !== undefined, 'x/y/width/height need to be defined' );
      
      this._rectX = x;
      this._rectY = y;
      this._rectWidth = width;
      this._rectHeight = height;
      this._rectArcWidth = arcWidth || 0;
      this._rectArcHeight = arcHeight || 0;
      this.invalidateRectangle();
    },
    
    isRounded: function() {
      return this._rectArcWidth !== 0 && this._rectArcHeight !== 0;
    },
    
    createRectangleShape: function() {
      sceneryAssert && sceneryAssert( isFinite( this._rectX ), 'A rectangle needs to have a finite x (' + this._rectX + ')' );
      sceneryAssert && sceneryAssert( isFinite( this._rectY ), 'A rectangle needs to have a finite x (' + this._rectY + ')' );
      sceneryAssert && sceneryAssert( this._rectWidth >= 0 && isFinite( this._rectWidth ),
                                      'A rectangle needs to have a non-negative finite width (' + this._rectWidth + ')' );
      sceneryAssert && sceneryAssert( this._rectHeight >= 0 && isFinite( this._rectHeight ),
                                      'A rectangle needs to have a non-negative finite height (' + this._rectHeight + ')' );
      sceneryAssert && sceneryAssert( this._rectArcWidth >= 0 && isFinite( this._rectArcWidth ),
                                      'A rectangle needs to have a non-negative finite arcWidth (' + this._rectArcWidth + ')' );
      sceneryAssert && sceneryAssert( this._rectArcHeight >= 0 && isFinite( this._rectArcHeight ),
                                      'A rectangle needs to have a non-negative finite arcHeight (' + this._rectArcHeight + ')' );
      // sceneryAssert && sceneryAssert( !this.isRounded() || ( this._rectWidth >= this._rectArcWidth * 2 && this._rectHeight >= this._rectArcHeight * 2 ),
      //                                 'The rounded sections of the rectangle should not intersect (the length of the straight sections shouldn\'t be negative' );
      
      if ( this.isRounded() ) {
        // copy border-radius CSS behavior in Chrome, where the arcs won't intersect, in cases where the arc segments at full size would intersect each other
        var maximumArcSize = Math.min( this._rectWidth / 2, this._rectHeight / 2 );
        return Shape.roundRectangle( this._rectX, this._rectY, this._rectWidth, this._rectHeight,
                                     Math.min( maximumArcSize, this._rectArcWidth ), Math.min( maximumArcSize, this._rectArcHeight ) );
      } else {
        return Shape.rectangle( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
      }
    },
    
    invalidateRectangle: function() {
      // setShape should invalidate the path and ensure a redraw
      this.setShape( this.createRectangleShape() );
    },
    
    computeShapeBounds: function() {
      // optimization, where we know our computed bounds will be just expanded by half the lineWidth if we are stroked (don't have to compute the stroke shape)
      return this._stroke ? this._shape.bounds.dilated( this._lineDrawingStyles.lineWidth / 2 ) : this._shape.bounds;
    },
    
    // accelerated hit detection for axis-aligned optionally-rounded rectangle
    // fast computation if it isn't rounded. if rounded, we check if a corner computation is needed (usually isn't), and only check that one needed corner
    containsPointSelf: function( point ) {
      var result = point.x >= this._rectX &&
                   point.x <= this._rectX + this._rectWidth &&
                   point.y >= this._rectY &&
                   point.y <= this._rectY + this._rectHeight;
      
      if ( !result || !this.isRounded() ) {
        return result;
      }
      
      // we are rounded and inside the logical rectangle (if it didn't have rounded corners)
      
      // closest corner arc's center (we assume the rounded rectangle's arcs are 90 degrees fully, and don't intersect)
      var closestCornerX, closestCornerY, guaranteedInside = false;
      
      // if we are to the inside of the closest corner arc's center, we are guaranteed to be in the rounded rectangle (guaranteedInside)
      if ( point.x < this._rectX + this._rectWidth / 2 ) {
        closestCornerX = this._rectX + this._rectArcWidth;
        guaranteedInside = guaranteedInside || point.x >= closestCornerX;
      } else {
        closestCornerX = this._rectX + this._rectWidth - this._rectArcWidth;
        guaranteedInside = guaranteedInside || point.x <= closestCornerX;
      }
      if ( guaranteedInside ) { return true; }
      
      if ( point.y < this._rectY + this._rectHeight / 2 ) {
        closestCornerY = this._rectY + this._rectArcHeight;
        guaranteedInside = guaranteedInside || point.y >= closestCornerY;
      } else {
        closestCornerY = this._rectY + this._rectHeight - this._rectArcHeight;
        guaranteedInside = guaranteedInside || point.y <= closestCornerY;
      }
      if ( guaranteedInside ) { return true; }
      
      // we are now in the rectangular region between the logical corner and the center of the closest corner's arc.
      
      // offset from the closest corner's arc center
      var offsetX = point.x - closestCornerX;
      var offsetY = point.y - closestCornerY;
      
      // normalize the coordinates so now we are dealing with a unit circle
      // (technically arc, but we are guaranteed to be in the area covered by the arc, so we just consider the circle)
      // NOTE: we are rounded, so both arcWidth and arcHeight are non-zero (this is well defined)
      offsetX /= this._rectArcWidth;
      offsetY /= this._rectArcHeight;
      
      offsetX *= offsetX;
      offsetY *= offsetY;
      return offsetX + offsetY <= 1; // return whether we are in the rounded corner. see the formula for an ellipse
    },
    
    // override paintCanvas with a faster version, since fillRect and drawRect don't affect the current default path
    paintCanvas: function( wrapper ) {
      var context = wrapper.context;
      
      // use the standard version if it's a rounded rectangle, since there is no Canvas-optimized version for that
      if ( this.isRounded() ) {
        return Path.prototype.paintCanvas.call( this, wrapper );
      }
      
      // TODO: how to handle fill/stroke delay optimizations here?
      if ( this._fill ) {
        this.beforeCanvasFill( wrapper ); // defined in Fillable
        context.fillRect( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
        this.afterCanvasFill( wrapper ); // defined in Fillable
      }
      if ( this._stroke ) {
        this.beforeCanvasStroke( wrapper ); // defined in Strokable
        context.strokeRect( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
        this.afterCanvasStroke( wrapper ); // defined in Strokable
      }
    },
    
    // create a rect instead of a path, hopefully it is faster in implementations
    createSVGFragment: function( svg, defs, group ) {
      return document.createElementNS( 'http://www.w3.org/2000/svg', 'rect' );
    },
    
    // optimized for the rect element instead of path
    updateSVGFragment: function( rect ) {
      // see http://www.w3.org/TR/SVG/shapes.html#RectElement
      rect.setAttribute( 'x', this._rectX );
      rect.setAttribute( 'y', this._rectY );
      rect.setAttribute( 'width', this._rectWidth );
      rect.setAttribute( 'height', this._rectHeight );
      rect.setAttribute( 'rx', this._rectArcWidth );
      rect.setAttribute( 'ry', this._rectArcHeight );
      
      rect.setAttribute( 'style', this.getSVGFillStyle() + this.getSVGStrokeStyle() );
    },
    
    getBasicConstructor: function( propLines ) {
      return 'new scenery.Rectangle( ' + this._rectX + ', ' + this._rectY + ', ' + 
                                         this._rectWidth + ', ' + this._rectHeight + ', ' +
                                         this._rectArcWidth + ', ' + this._rectArcHeight + ', {' + propLines + '} )';
    }
    
  } );
  
  function addRectProp( capitalizedShort ) {
    var getName = 'getRect' + capitalizedShort;
    var setName = 'setRect' + capitalizedShort;
    var privateName = '_rect' + capitalizedShort;
    
    Rectangle.prototype[getName] = function() {
      return this[privateName];
    };
    
    Rectangle.prototype[setName] = function( value ) {
      this[privateName] = value;
      this.invalidateRectangle();
      return this;
    };
    
    Object.defineProperty( Rectangle.prototype, 'rect' + capitalizedShort, {
      set: Rectangle.prototype[setName],
      get: Rectangle.prototype[getName]
    } );
  }
  
  addRectProp( 'X' );
  addRectProp( 'Y' );
  addRectProp( 'Width' );
  addRectProp( 'Height' );
  addRectProp( 'ArcWidth' );
  addRectProp( 'ArcHeight' );
  
  // not adding mutators for now
  Rectangle.prototype._mutatorKeys = [ 'rectX', 'rectY', 'rectWidth', 'rectHeight', 'rectArcWidth', 'rectArcHeight' ].concat( Path.prototype._mutatorKeys );
  
  return Rectangle;
} );



// Copyright 2002-2013, University of Colorado

/**
 * Basic dragging for a node.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/SimpleDragHandler',['require','DOT/Matrix3','SCENERY/scenery'], function( require ) {
  
  
  var Matrix3 = require( 'DOT/Matrix3' );
  
  var scenery = require( 'SCENERY/scenery' );
  
  /*
   * Allowed options: {
   *    allowTouchSnag: false // allow touch swipes across an object to pick it up,
   *    mouseButton: 0        // allow changing the mouse button that activates the drag listener. -1 should activate on any mouse button, 0 on left, 1 for middle, 2 for right, etc.
   *    start: null           // if non-null, called when a drag is started. start( event, trail )
   *    drag: null            // if non-null, called when the user moves something with a drag (not a start or end event).
   *                                                                         drag( event, trail )
   *    end: null             // if non-null, called when a drag is ended.   end( event, trail )
   *    translate:            // if this exists, translate( { delta: _, oldPosition: _, position: _ } ) will be called instead of directly translating the node
   * }
   */
  scenery.SimpleDragHandler = function SimpleDragHandler( options ) {
    var handler = this;
    
    this.options = _.extend( {
      allowTouchSnag: false,
      mouseButton: 0
    }, options );
    
    this.dragging              = false;     // whether a node is being dragged with this handler
    this.pointer               = null;      // the pointer doing the current dragging
    this.trail                 = null;      // stores the path to the node that is being dragged
    this.transform             = null;      // transform of the trail to our node (but not including our node, so we can prepend the deltas)
    this.node                  = null;      // the node that we are handling the drag for
    this.lastDragPoint         = null;      // the location of the drag at the previous event (so we can calculate a delta)
    this.startTransformMatrix  = null;      // the node's transform at the start of the drag, so we can reset on a touch cancel
    this.mouseButton           = undefined; // tracks which mouse button was pressed, so we can handle that specifically
    // TODO: consider mouse buttons as separate pointers?
    
    // if an ancestor is transformed, pin our node
    this.transformListener = {
      transform: function( args ) {
        if ( !handler.trail.isExtensionOf( args.trail, true ) ) {
          return;
        }
        
        var newMatrix = args.trail.getTransform().getMatrix();
        var oldMatrix = handler.transform.getMatrix();
        
        // if A was the trail's old transform, B is the trail's new transform, we need to apply (B^-1 A) to our node
        handler.node.prependMatrix( newMatrix.inverted().timesMatrix( oldMatrix ) );
        
        // store the new matrix so we can do deltas using it now
        handler.transform.set( newMatrix );
      }
    };
    
    // this listener gets added to the pointer when it starts dragging our node
    this.dragListener = {
      // mouse/touch up
      up: function( event ) {
        sceneryAssert && sceneryAssert( event.pointer === handler.pointer );
        if ( !event.pointer.isMouse || event.domEvent.button === handler.mouseButton ) {
          var saveCurrentTarget = event.currentTarget;
          event.currentTarget = handler.node; // #66: currentTarget on a pointer is null, so set it to the node we're dragging
          handler.endDrag( event );
          event.currentTarget = saveCurrentTarget; // be polite to other listeners, restore currentTarget
        }
      },
      
      // touch cancel
      cancel: function( event ) {
        sceneryAssert && sceneryAssert( event.pointer === handler.pointer );

        var saveCurrentTarget = event.currentTarget;
        event.currentTarget = handler.node; // #66: currentTarget on a pointer is null, so set it to the node we're dragging
        handler.endDrag( event );
        event.currentTarget = saveCurrentTarget; // be polite to other listeners, restore currentTarget

        // since it's a cancel event, go back!
        if ( !handler.transform ) {
          handler.node.setMatrix( handler.startTransformMatrix );
        }
      },
      
      // mouse/touch move
      move: function( event ) {
        sceneryAssert && sceneryAssert( event.pointer === handler.pointer );
        
        var delta = handler.transform.inverseDelta2( handler.pointer.point.minus( handler.lastDragPoint ) );
        
        // move by the delta between the previous point, using the precomputed transform
        // prepend the translation on the node, so we can ignore whatever other transform state the node has
        if ( handler.options.translate ) {
          var translation = handler.node.getTransform().getMatrix().getTranslation();
          handler.options.translate( {
            delta: delta,
            oldPosition: translation,
            position: translation.plus( delta )
          } );
        } else {
          handler.node.translate( delta, true );
        }
        handler.lastDragPoint = handler.pointer.point;
        
        if ( handler.options.drag ) {
          // TODO: consider adding in a delta to the listener
          // TODO: add the position in to the listener
          var saveCurrentTarget = event.currentTarget;
          event.currentTarget = handler.node; // #66: currentTarget on a pointer is null, so set it to the node we're dragging
          handler.options.drag( event, handler.trail ); // new position (old position?) delta
          event.currentTarget = saveCurrentTarget; // be polite to other listeners, restore currentTarget
        }
      }
    };
  };
  var SimpleDragHandler = scenery.SimpleDragHandler;
  
  SimpleDragHandler.prototype = {
    constructor: SimpleDragHandler,
    
    startDrag: function( event ) {
      // set a flag on the pointer so it won't pick up other nodes
      event.pointer.dragging = true;
      event.pointer.addInputListener( this.dragListener );
      // event.trail.rootNode().addEventListener( this.transformListener ); // TODO: replace with new parent transform listening solution
      
      // set all of our persistent information
      this.dragging = true;
      this.pointer = event.pointer;
      this.trail = event.trail.subtrailTo( event.currentTarget, true );
      this.transform = this.trail.getTransform();
      this.node = event.currentTarget;
      this.lastDragPoint = event.pointer.point;
      this.startTransformMatrix = event.currentTarget.getMatrix();
      // event.domEvent may not exist if this is touch-to-snag
      this.mouseButton = event.pointer.isMouse ? event.domEvent.button : undefined;
      
      if ( this.options.start ) {
        this.options.start( event, this.trail );
      }
    },
    
    endDrag: function( event ) {
      this.pointer.dragging = false;
      this.pointer.removeInputListener( this.dragListener );
      // this.trail.rootNode().removeEventListener( this.transformListener ); // TODO: replace with new parent transform listening solution
      this.dragging = false;
      
      if ( this.options.end ) {
        this.options.end( event, this.trail );
      }
    },
    
    tryToSnag: function( event ) {
      // don't allow drag attempts that use the wrong mouse button (-1 indicates any mouse button works)
      if ( event.pointer.isMouse && event.domEvent && this.options.mouseButton !== event.domEvent.button && this.options.mouseButton !== -1 ) {
        return;
      }
      
      // only start dragging if the pointer isn't dragging anything, we aren't being dragged, and if it's a mouse it's button is down
      if ( !this.dragging && !event.pointer.dragging ) {
        this.startDrag( event );
      }
    },
    
    /*---------------------------------------------------------------------------*
    * events called from the node input listener
    *----------------------------------------------------------------------------*/
    
    // mouse/touch down on this node
    down: function( event ) {
      this.tryToSnag( event );
    },
    
    // touch enters this node
    touchenter: function( event ) {
      // allow touches to start a drag by moving "over" this node
      if ( this.options.allowTouchSnag ) {
        this.tryToSnag( event );
      }
    }
  };
  
  return SimpleDragHandler;
} );



// Copyright 2002-2013, University of Colorado Boulder

/**
 * Provides access to font-awesome glyphs as scenery nodes.
 *
 * @author Sam Reid
 */
define('SUN/FontAwesomeNode',['require','SCENERY/nodes/Image','SCENERY/nodes/VBox','SCENERY/nodes/DOM','SCENERY/nodes/Node','SCENERY/nodes/HBox','SCENERY/Scene','SCENERY/nodes/Text','SCENERY/nodes/Path','SCENERY/nodes/Rectangle','KITE/Shape','SCENERY/input/SimpleDragHandler','DOT/Bounds2','DOT/Matrix3','PHET_CORE/inherit'], function( require ) {
  

  var Image = require( 'SCENERY/nodes/Image' );
  var VBox = require( 'SCENERY/nodes/VBox' );
  var DOM = require( 'SCENERY/nodes/DOM' );
  var Node = require( 'SCENERY/nodes/Node' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var Scene = require( 'SCENERY/Scene' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var inherit = require( 'PHET_CORE/inherit' );

  // Generated using chipper.FontAwesomeToScenery
  var icons = {
    fullscreen: "M1283 995l-355 -355l355 -355l144 144q29 31 70 14q39 -17 39 -59v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l144 144l-355 355l-355 -355l144 -144q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l144 -144 l355 355l-355 355l-144 -144q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v448q0 26 19 45t45 19h448q42 0 59 -40q17 -39 -14 -69l-144 -144l355 -355l355 355l-144 144q-31 30 -14 69q17 40 59 40h448q26 0 45 -19t19 -45v-448q0 -42 -39 -59q-13 -5 -25 -5q-26 0 -45 19z",
    reorder: "M1536 192v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 704v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 1216v-128q0 -26 -19 -45 t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z",
    home: "M1408 544v-480q0 -26 -19 -45t-45 -19h-384v384h-256v-384h-384q-26 0 -45 19t-19 45v480q0 1 0.5 3t0.5 3l575 474l575 -474q1 -2 1 -6zM1631 613l-62 -74q-8 -9 -21 -11h-3q-13 0 -21 7l-692 577l-692 -577q-12 -8 -24 -7q-13 2 -21 11l-62 74q-8 10 -7 23.5t11 21.5 l719 599q32 26 76 26t76 -26l244 -204v195q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-408l219 -182q10 -8 11 -21.5t-7 -23.5z",
    volume_off: "M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45z",
    volume_up: "M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142zM1408 640q0 -153 -85 -282.5t-225 -188.5q-13 -5 -25 -5q-27 0 -46 19t-19 45q0 39 39 59q56 29 76 44q74 54 115.5 135.5t41.5 173.5t-41.5 173.5 t-115.5 135.5q-20 15 -76 44q-39 20 -39 59q0 26 19 45t45 19q13 0 26 -5q140 -59 225 -188.5t85 -282.5zM1664 640q0 -230 -127 -422.5t-338 -283.5q-13 -5 -26 -5q-26 0 -45 19t-19 45q0 36 39 59q7 4 22.5 10.5t22.5 10.5q46 25 82 51q123 91 192 227t69 289t-69 289 t-192 227q-36 26 -82 51q-7 4 -22.5 10.5t-22.5 10.5q-39 23 -39 59q0 26 19 45t45 19q13 0 26 -5q211 -91 338 -283.5t127 -422.5z",
    check: "M1408 606v-318q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-10 -10 -23 -10q-3 0 -9 2q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832 q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v254q0 13 9 22l64 64q10 10 23 10q6 0 12 -3q20 -8 20 -29zM1639 1095l-814 -814q-24 -24 -57 -24t-57 24l-430 430q-24 24 -24 57t24 57l110 110q24 24 57 24t57 -24l263 -263l647 647q24 24 57 24t57 -24l110 -110 q24 -24 24 -57t-24 -57z",
    check_empty: "M1120 1280h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v832q0 66 -47 113t-113 47zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q119 0 203.5 -84.5t84.5 -203.5z",
    undo: "M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-179 0 -336.5 76t-266 213t-147.5 312q-3 14 7 27q9 12 25 12h199q23 0 30 -23q50 -162 185 -261.5t304 -99.5q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5t-163.5 109.5 t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298z"
  };

  function FontAwesomeNode( iconName, options ) {

    // default values
    options = _.extend( {
      fill: "#000",
      //Font awesome nodes are expensive to pick (and have a lot of holes in them which you may wish to pick anyways, such as the door of the 'home' icon, so don't pick by default
      pickable: false
    }, options );

    // add internal values required by supertype constructor
    options = _.extend( options, {
      matrix: new Matrix3( 0.025, 0, 0, 0, -0.025, 0, 0, 0, 1 )
    } );

    Path.call( this, new Shape( icons[iconName] ), options );
  }

  inherit( Path, FontAwesomeNode );

  return FontAwesomeNode;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Control panel around a content node.
 * Dynamically adjusts its size to fit its contents.
 *
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/Panel',['require','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Rectangle'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );

  /**
   * @param {Node} content
   * @param {object} options
   * @constructor
   */
  function Panel( content, options ) {

    var thisNode = this;

    // default options
    options = _.extend( {
      fill: 'white',
      stroke: 'black',
      lineWidth: 1, // width of the background border
      xMargin: 5,
      yMargin: 5,
      cornerRadius: 10, // radius of the rounded corners on the background
      resize: true, // dynamically resize when content bounds change?
      backgroundPickable: false
    }, options );

    Node.call( thisNode );

    // correct size will be set by updateBackground
    var background = new Rectangle( 0, 0, 1, 1, {stroke: options.stroke, lineWidth: options.lineWidth, fill: options.fill, pickable: options.backgroundPickable} );
    this.addChild( background );
    this.addChild( content );

    // Adjust the background size to match the content.
    var updateBackground = function() {
      background.setRect( 0, 0, content.width + ( 2 * options.xMargin ), content.height + ( 2 * options.yMargin ), options.cornerRadius, options.cornerRadius );
      content.centerX = background.centerX;
      content.centerY = background.centerY;
    };
    if ( options.resize ) {
      content.addEventListener( 'bounds', function() {
        updateBackground();
      } );
    }
    updateBackground();

    // Apply options after the layout is done, so that options that use the bounds will work properly.
    this.mutate( options );
  }

  inherit( Node, Panel );

  return Panel;
} );
// Copyright 2002-2013, University of Colorado Boulder

//TODO this should extend sun.Button
//TODO this should handle wiring up the callback that goes to the home screen, currently done in NavigationBar
/**
 * The Home button that appears in the navigation bar.
 */
define('SCENERY_PHET/HomeButton',['require','SUN/FontAwesomeNode','PHET_CORE/inherit','SCENERY/nodes/Node','KITE/Shape'], function( require ) {
  

  // imports
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Shape = require( 'KITE/Shape' );

  function HomeButton( options ) {

    options = _.extend( { cursor: 'pointer' }, options );

    Node.call( this, options );

    var icon = new FontAwesomeNode( 'home', { fill: '#fff', scale: 0.75 } );
    this.mouseArea = this.touchArea = Shape.rectangle( icon.bounds.minX, icon.bounds.minY, icon.bounds.width, icon.bounds.height );
    this.addChild( icon );
  }

  inherit( Node, HomeButton );

  return HomeButton;
} );
// Copyright 2002-2013, University of Colorado Boulder

//TODO #4: add support for consecutive line breaks, eg 'Hello\n\nWorld'
//TODO #4: add support for leading (vertical space) between lines
/**
 * MultiLine plain text, with alignment.
 * The line break character is '\n'.
 * Specify alignment via the 'align' option.
 * Text node options can be specified to style the text.
 * <p>
 * Example: new MultiLineText( 'Hello\nWorld', { align: 'left', font: new PhetFont(20), fill: 'red' } );
 *
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/MultiLineText',['require','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Text','SCENERY/nodes/VBox'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Text = require( 'SCENERY/nodes/Text' );
  var VBox = require( 'SCENERY/nodes/VBox' );

  function MultiLineText( text, options ) {

    var thisNode = this;

    thisNode._options = options = _.extend( {
      align: 'center' // 'center', 'left' or 'right' (as supported by VBox)
    }, options );

    Node.call( thisNode );
    thisNode.text = text;
    thisNode.mutate( _.omit( options, 'align' ) ); // mutate after removing options that are specific to this subtype
  }

  inherit( Node, MultiLineText, {
      get text() {
        return this._text;
      },
      set text( string ) {
        var thisNode = this;
        thisNode._text = string;
        thisNode.children = [ new VBox( {
          children: string.split( '\n' ).map( function( line ) {
            return new Text( line, _.omit( thisNode._options, 'align' ) );
          } ),
          align: thisNode._options.align
        } )];
      }
    }
  );

  return MultiLineText;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * The view portion of a Screen.
 * Specifies the layout strategy.
 * TODO: should extend this to be compatible with the Sim.js framework.
 *
 * @author Sam Reid
 */
define('JOIST/ScreenView',['require','SCENERY/nodes/Node','PHET_CORE/inherit','DOT/Bounds2','SCENERY/nodes/Path','KITE/Shape'], function( require ) {
  

  var Node = require( 'SCENERY/nodes/Node' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );

  function ScreenView( options ) {
    Node.call( this, options );

    // Show the layoutBounds
    if ( window.phetcommon.getQueryParameter( 'dev' ) ) {
      this.addChild( new Path( Shape.bounds( this.layoutBounds ), { stroke: 'red', lineWidth: 3, pickable: false } ) );
    }
  }

  inherit( Node, ScreenView, {

    //Default to width and height for iPad2, iPad3, iPad4 running Safari with default tabs and decorations
    //Simulations can change this to provide their own sizes or aspect ratios
    //TODO: the code that uses these bounds needs to account for the minX and minY values if they are overriden in subclasses
    layoutBounds: new Bounds2( 0, 0, 768, 504 ),

    //Get the scale to use for laying out the sim components and the navigation bar, so its size will track with the sim size
    getLayoutScale: function( width, height ) {
      return Math.min( width / this.layoutBounds.width, height / this.layoutBounds.height );
    },

    //Default layout function uses the layoutWidth and layoutHeight to scale the content (based on whichever is more limiting: width or height)
    //and centers the content in the screen vertically and horizontally
    //This function can be replaced by subclasses that wish to perform their own custom layout.
    layout: function( width, height ) {
      this.resetTransform();

      var scale = this.getLayoutScale( width, height );
      this.setScaleMagnitude( scale );

      //center vertically
      if ( scale === width / this.layoutBounds.width ) {
        this.translate( 0, (height - this.layoutBounds.height * scale) / 2 / scale );
      }

      //center horizontally
      else if ( scale === height / this.layoutBounds.height ) {
        this.translate( (width - this.layoutBounds.width * scale) / 2 / scale, 0 );
      }
    }
  } );

  return ScreenView;
} );

// Copyright 2002-2013, University of Colorado

/**
 * HTML Text, with the same interface as Text
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/HTMLText',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/nodes/Text'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );
  var Text = require( 'SCENERY/nodes/Text' ); // inherits from Text
  
  scenery.HTMLText = function HTMLText( text, options ) {
    // internal flag for Text
    this._isHTML = true;
    
    Text.call( this, text, options );
  };
  var HTMLText = scenery.HTMLText;
  
  inherit( Text, HTMLText, {} );
  
  return HTMLText;
} );



// Copyright 2002-2013, University of Colorado Boulder

/**
 * Encapsulation of the font used for PhET simulations.
 * Provides PhET-specific defaults, and guarantees a fallback for font family.
 * <p>
 * Sample use:
 * new PhetFont( { family: 'Futura', size: 24, weight: 'bold' } )
 * new PhetFont( 24 )
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/PhetFont',['require','ASSERT/assert','SCENERY/util/Font','PHET_CORE/inherit'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'scenery-phet' );
  var Font = require( 'SCENERY/util/Font' );
  var inherit = require( 'PHET_CORE/inherit' );

  /**
   * @param {Number|*} options if number this is the font size, otherwise same options as scenery.Font
   * @constructor
   */
  function PhetFont( options ) {

    // convenience constructor: new PhetFont( {number|string} size )
    if ( typeof options === 'number' || typeof options === 'string' ) {
      options = { size: options };
    }

    // PhET defaults
    options = _.extend( {
      family: 'Arial'
    }, options );

    // Guarantee a fallback family
    assert && assert( options.family );
    options.family = options.family + ', sans-serif';

    Font.call( this, options );
  }

  return inherit( Font, PhetFont );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Shows the about dialog.
 *
 * @author Sam Reid
 */
define('JOIST/AboutDialog',['require','SCENERY/nodes/Node','SCENERY/nodes/VBox','SCENERY/nodes/Rectangle','SCENERY/nodes/Text','PHET_CORE/inherit','SCENERY_PHET/MultiLineText','JOIST/ScreenView','SUN/Panel','SCENERY/nodes/HTMLText','SCENERY_PHET/PhetFont'], function( require ) {
  

  var Node = require( 'SCENERY/nodes/Node' );
  var VBox = require( 'SCENERY/nodes/VBox' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Text = require( 'SCENERY/nodes/Text' );
  var inherit = require( 'PHET_CORE/inherit' );
  var MultiLineText = require( 'SCENERY_PHET/MultiLineText' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var Panel = require( 'SUN/Panel' );
  var HTMLText = require( 'SCENERY/nodes/HTMLText' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );

  // constants
  var SOFTWARE_AGREEMENT_URL = 'http://phet.colorado.edu/about/software-agreement_v7.htm';

  /**
   * @param {Sim} sim
   * @constructor
   */
  function AboutDialog( sim ) {
    var aboutDialog = this;

    //Use view, to help center and scale content
    ScreenView.call( this, {renderer: 'svg'} );

    var softwareAgreementLink = new HTMLText( '<a href="#" onclick="return false;">Software Agreement</a>', {
      font: new PhetFont( 14 ),
      renderer: 'dom',
      interactive: true // don't prevent default on the events
    } ); //TODO i18n
    softwareAgreementLink.addInputListener( {
      up: function( evt ) {
        evt.handle(); // don't close the dialog
      },
      upImmediate: function( evt ) {
        var aboutDialogWindow = window.open( SOFTWARE_AGREEMENT_URL, '_blank' );
        aboutDialogWindow.focus();
      }
    } );
    
    var debugLink = 'http://phet.colorado.edu/files/troubleshooting/' +
                    '?sim=' + encodeURIComponent( sim.name ) +
                    '&version=' + encodeURIComponent( sim.version ) +
                    '&url=' + encodeURIComponent( window.location.href );
    var debuggingInfoLink = new HTMLText( '<a href="' + debugLink + '" onclick="return false;">Debugging Information (for bug reports)</a>', {
      font: new PhetFont( 12 ),
      renderer: 'dom',
      interactive: true // don't prevent default on the events
    } ); //TODO i18n
    debuggingInfoLink.addInputListener( {
      up: function( evt ) {
        evt.handle(); // don't close the dialog
      },
      upImmediate: function( evt ) {
        var debugWindow = window.open( debugLink, '_blank' );
        debugWindow.focus();
      }
    } );

    var content = new VBox( { align: 'left', spacing: 5, children: [
      new Text( 'PhET Interactive Simulations', { font: new PhetFont( 16 ) } ),
      new Text( 'Copyright © 2004-2013 University of Colorado Boulder', { font: new PhetFont( 12 ) } ),
      new Text( ' ', { font: new PhetFont( 28 ) } ),
      new Text( sim.name, { font: new PhetFont( 28 ) } ),
      new Text( 'version ' + sim.version, { font: new PhetFont( 20 ) } ),
      new Text( ' ' ),
      new MultiLineText( sim.credits, { align: 'left', font: new PhetFont( 12 ) } ),
      new Text( ' ' ),
      new MultiLineText( sim.thanks, { align: 'left', font: new PhetFont( 12 ) } ),
      new Text( ' ' ),
      softwareAgreementLink,
      new Text( ' ' ),
      debuggingInfoLink
    ]} );

    //Show a gray overlay that will help focus on the about dialog, and prevent clicks on the sim while the dialog is up
    this.addChild( new Panel( content, {centerX: this.layoutBounds.centerX, centerY: this.layoutBounds.centerY, xMargin: 20, yMargin: 20 } ) );

    function resize() {
      aboutDialog.layout( $( window ).width(), $( window ).height() );
    }

    //Fit to the window and render the initial scene
    $( window ).resize( resize );
    resize();
  }

  inherit( ScreenView, AboutDialog );

  return AboutDialog;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Basic down/up pointer handling for a Node, so that it's easy to handle buttons
 *
 * TODO: test hand handle down, go off screen, up. How to handle that properly?
 * TODO: tests
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/DownUpListener',['require','SCENERY/scenery','SCENERY/util/Trail'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/util/Trail' );
  
  /*
   * The 'trail' parameter passed to down/upInside/upOutside will end with the node to which this DownUpListener has been added.
   *
   * Allowed options: {
   *    mouseButton: 0  // The mouse button to use: left: 0, middle: 1, right: 2, see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
   *    down: null      // down( event, trail ) is called when the pointer is pressed down on this node
   *                    // (and another pointer is not already down on it).
   *    up: null        // up( event, trail ) is called after 'down', regardless of the pointer's current location.
   *                    // Additionally, it is called AFTER upInside or upOutside, whichever is relevant
   *    upInside: null  // upInside( event, trail ) is called after 'down', when the pointer is released inside
   *                    // this node (it or a descendant is the top pickable node under the pointer)
   *    upOutside: null // upOutside( event, trail ) is called after 'down', when the pointer is released outside
   *                    // this node (it or a descendant is the not top pickable node under the pointer, even if the
   *                    // same instance is still directly under the pointer)
   * }
   */
  scenery.DownUpListener = function DownUpListener( options ) {
    var handler = this;
    
    this.options = _.extend( {
      mouseButton: 0 // allow a different mouse button 
    }, options );
    this.isDown = false;   // public, whether this listener is down
    this.downCurrentTarget = null; // 'up' is handled via a pointer lister, which will have null currentTarget, so save the 'down' currentTarget
    this.downTrail = null;
    this.pointer = null;
    
    // this listener gets added to the pointer on a 'down'
    this.downListener = {
      // mouse/touch up
      up: function( event ) {
        sceneryAssert && sceneryAssert( event.pointer === handler.pointer );
        if ( !event.pointer.isMouse || event.domEvent.button === handler.options.mouseButton ) {
          handler.buttonUp( event );
        }
      },
      
      // touch cancel
      cancel: function( event ) {
        sceneryAssert && sceneryAssert( event.pointer === handler.pointer );
        handler.buttonUp( event );
      }
    };
  };
  var DownUpListener = scenery.DownUpListener;
  
  DownUpListener.prototype = {
    constructor: DownUpListener,
    
    buttonDown: function( event ) {
      // already down from another pointer, don't do anything
      if ( this.isDown ) { return; }
      
      // ignore other mouse buttons
      if ( event.pointer.isMouse && event.domEvent.button !== this.options.mouseButton ) { return; }
      
      // add our listener so we catch the up wherever we are
      event.pointer.addInputListener( this.downListener );
      
      this.isDown = true;
      this.downCurrentTarget = event.currentTarget;
      this.downTrail = event.trail.subtrailTo( event.currentTarget, false );
      this.pointer = event.pointer;
      
      if ( this.options.down ) {
        this.options.down( event, this.downTrail );
      }
    },
    
    buttonUp: function( event ) {
      this.isDown = false;
      this.pointer.removeInputListener( this.downListener );

      var currentTargetSave = event.currentTarget;
      event.currentTarget = this.downCurrentTarget; // up is handled by a pointer listener, so currentTarget would be null.
      if ( this.options.upInside || this.options.upOutside ) {
        var scene = this.downTrail.rootNode();
        var trailUnderPointer = event.trail;
        
        // TODO: consider changing this so that it just does a hit check and ignores anything in front?
        var isInside = trailUnderPointer.isExtensionOf( this.downTrail, true );
        
        if ( isInside && this.options.upInside ) {
          this.options.upInside( event, this.downTrail );
        } else if ( !isInside && this.options.upOutside ) {
          this.options.upOutside( event, this.downTrail );
        }
      }
      if ( this.options.up ) {
        this.options.up( event, this.downTrail );
      }
      event.currentTarget = currentTargetSave; // be polite to other listeners, restore currentTarget
    },
    
    /*---------------------------------------------------------------------------*
    * events called from the node input listener
    *----------------------------------------------------------------------------*/
    
    // mouse/touch down on this node
    down: function( event ) {
      this.buttonDown( event );
    }
  };
  
  return DownUpListener;
} );



// Copyright 2002-2013, University of Colorado

/**
 * Basic button handling.
 *
 * Uses 4 states:
 * up: mouse not over, not pressed
 * over: mouse over, not pressed
 * down: mouse over, pressed
 * out: mouse not over, pressed
 *
 * TODO: offscreen handling
 * TODO: fix enter/exit edge cases for moving nodes or add/remove child, and when touches are created
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/input/ButtonListener',['require','SCENERY/scenery','SCENERY/util/Trail','PHET_CORE/inherit','SCENERY/input/DownUpListener'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/util/Trail' );
  var inherit = require( 'PHET_CORE/inherit' );
  
  var DownUpListener = require( 'SCENERY/input/DownUpListener' );
  
  /**
   * Options for the ButtonListener:
   *
   * mouseButton: 0
   * fireOnDown: false // default is to fire on 'up' after 'down', but passing fireOnDown: true will fire on 'down' instead
   * up: null          // Called on an 'up' state change, as up( event, oldState )
   * over: null        // Called on an 'over' state change, as over( event, oldState )
   * down: null        // Called on an 'down' state change, as down( event, oldState )
   * out: null         // Called on an 'out' state change, as out( event, oldState )
   * fire: null        // Called on a state change to/from 'down' (depending on fireOnDown), as fire( event ). Called after the triggering up/over/down event.
   */
  scenery.ButtonListener = function ButtonListener( options ) {

    this.buttonState = 'up'; // public: 'up', 'over', 'down' or 'out'
    
    this._overCount = 0; // how many pointers are over us (track a count, so we can handle multiple pointers gracefully)
    
    this._buttonOptions = options; // store the options object so we can call the callbacks
    
    var buttonListener = this;
    DownUpListener.call( this, {

      mouseButton: options.mouseButton || 0, // forward the mouse button, default to 0 (LMB)
      
      down: function( event, trail ) {
        buttonListener.setButtonState( event, 'down' );
      },
      
      up: function( event, trail ) {
        buttonListener.setButtonState( event, buttonListener._overCount > 0 ? 'over' : 'up' );
      }
    } );
  };

  var ButtonListener = scenery.ButtonListener;
  
  inherit( DownUpListener, ButtonListener, {

    setButtonState: function( event, state ) {
      if ( state !== this.buttonState ) {
        var oldState = this.buttonState;
        
        this.buttonState = state;
        
        if ( this._buttonOptions[state] ) {
          this._buttonOptions[state]( event, oldState );
        }
        
        if ( this._buttonOptions.fire &&
             this._overCount > 0 &&
             ( this._buttonOptions.fireOnDown ? ( state === 'down' ) : ( oldState === 'down' ) ) ) {
          this._buttonOptions.fire( event );
        }
      }
    },
    
    enter: function( event ) {
      this._overCount++;
      if ( this._overCount === 1 ) {
        this.setButtonState( event, this.isDown ? 'down' : 'over' );
      }
    },

    exit: function( event ) {
      sceneryAssert && sceneryAssert( this._overCount > 0, 'Exit events not matched by an enter' );
      this._overCount--;
      if ( this._overCount === 0 ) {
        this.setButtonState( event, this.isDown ? 'out' : 'up' );
      }
    }
  } );

  //TODO delete this after work is completed on sun.Button and scenery.ButtonListener
  ButtonListener.TEST_LISTENER = new ButtonListener( {

    up: function( event, oldState ) {
      console.log( "ButtonListener.up oldState=" + oldState );
    },

    over: function( event, oldState ) {
      console.log( "ButtonListener.over oldState=" + oldState );
    },

    down: function( event, oldState ) {
      console.log( "ButtonListener.down oldState=" + oldState );
    },

    out: function( event, oldState ) {
      console.log( "ButtonListener.out oldState=" + oldState );
    },

    fire: function( event ) {
      console.log( "ButtonListener.fire" );
    }
  } );

  return ButtonListener;
} );



// Copyright 2002-2013, University of Colorado Boulder

/**
 * Check box.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/CheckBox',['require','SCENERY/input/ButtonListener','SUN/FontAwesomeNode','SCENERY/nodes/Image','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Rectangle','KITE/Shape','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var Text = require( 'SCENERY/nodes/Text' );

  /**
   * @param {Node} content
   * @param {Property<Boolean>} property
   * @constructor
   * @param options
   */
  function CheckBox( content, property, options ) {
    var checkBox = this;
    options = _.extend( {
      spacing: 5,
      boxScale: 0.6,
      cursor: 'pointer',
      checkBoxColor: 'black',
      checkBoxColorDisabled: 'gray',
      touchAreaTopPadding: 0,
      touchAreaBottomPadding: 0,
      touchAreaLeftPadding: 0,
      touchAreaRightPadding: 0
    }, options );

    var thisNode = this;
    Node.call( this );

    // save this stuff for use in prototype functions
    thisNode._options = options;
    thisNode._content = content;
    thisNode._enabled = true;

    var x = options.boxScale / 0.75;

    //Make the background white.  Until we are creating our own shapes, just put a white rectangle behind the font awesome check box icons
    var whiteBackground = new Rectangle( 0, -25 * x, 25 * x, 25 * x, 5 * x, 5 * x, {fill: 'white'} );

    thisNode._checkedNode = new FontAwesomeNode( 'check', { scale: options.boxScale, fill: options.checkBoxColor } );
    thisNode._uncheckedNode = new FontAwesomeNode( 'check_empty', { scale: options.boxScale, fill: options.checkBoxColor } );

    thisNode.addChild( whiteBackground );
    thisNode.addChild( thisNode._checkedNode );
    thisNode.addChild( thisNode._uncheckedNode );
    thisNode.addChild( content );

    content.left = thisNode._checkedNode.right + options.spacing;
    content.centerY = thisNode._checkedNode.centerY;

    // put a rectangle on top of everything to prevent dead zones which clicking
    thisNode.mouseArea = thisNode.touchArea = Shape.rectangle(
      thisNode.left - options.touchAreaLeftPadding, thisNode.top - options.touchAreaTopPadding,
      thisNode.width + options.touchAreaLeftPadding + options.touchAreaRightPadding, thisNode.height + options.touchAreaTopPadding + options.touchAreaBottomPadding );

    content.pickable = false; // since there's a pickable rectangle on top of content

    // interactivity
    thisNode.addInputListener( new ButtonListener( {
      fire: function() {
        if ( thisNode._enabled ) {
          property.value = !property.value;
        }
      }
    } ) );

    // sync with property
    property.link( function( checked ) {
      thisNode._checkedNode.visible = checked;
      thisNode._uncheckedNode.visible = !checked;
    } );

    //Add accessibility
    thisNode.addPeer( '<input type="checkbox">', {click: function() {property.value = !property.value;}, label: options.label} );
    property.link( function( value ) {
      _.each( checkBox.instances, function( instance ) {

        //Make sure accessibility is enabled, then apply the change to the peer
        _.each( instance.peers, function( peer ) {
          peer.element.setAttribute( 'checked', value );
        } );
      } );
    } );

    // Apply additional options
    thisNode.mutate( options );
  }

  return inherit( Node, CheckBox, {

    // prototype properties

    get enabled() { return this._enabled; },

    set enabled( value ) {

      this._enabled = value;

      // set the color of the check box icons
      this._checkedNode.fill = value ? this._options.checkBoxColor : this._options.checkBoxColorDisabled;
      this._uncheckedNode.fill = this._checkedNode.fill;
      
      // enable/disable the content, if it supports it
      if ( this._content.setEnabled ) {
        this._content.setEnabled( value );
      }
    }
    
  }, {

    // static properties

    /**
     * Factory method, creates a check box with a text label and optional icon.
     * @param {String} text
     * @param {*} textOptions options that apply to the text, same as scenery.Text
     * @param {Property<Boolean>} property
     * @returns {CheckBox}
     */
    createTextCheckBox: function( text, textOptions, property, checkBoxOptions ) {

      textOptions = _.extend( {
        fill: 'black',
        fillDisabled: 'rgb(220,220,220)'
      }, textOptions );

      checkBoxOptions = _.extend( {
        icon: null,  // an optional node, added to the right of the text
        iconSpacing: 5
      }, checkBoxOptions );

      var content = new Node();

      // text
      var textNode = new Text( text, textOptions );
      content.addChild( textNode );

      // options icon
      if ( checkBoxOptions.icon ) {
        content.addChild( checkBoxOptions.icon );
        //TODO support different layouts of text and image?
        checkBoxOptions.icon.left = textNode.right + checkBoxOptions.iconSpacing;
        checkBoxOptions.icon.centerY = textNode.centerY;
      }

      content.setEnabled = function( enabled ) {
        textNode.fill = enabled ? textOptions.fill : textOptions.fillDisabled;
        // enabled/disable the content if it implements setEnabled
        if ( checkBoxOptions.icon && checkBoxOptions.icon.setEnabled ) {
          checkBoxOptions.icon.setEnabled( enabled );
        }
      };

      return new CheckBox( content, property, checkBoxOptions );
    }
  } );
} );
// Copyright 2002-2013, University of Colorado Boulder

define('AXON/axon',['require'], function( require ) {
  

  var axon = {};

  // will be filled in by other modules
  return axon;
} );

// Copyright 2002-2013, University of Colorado Boulder

/*
 * PhET Simulations can be recorded and played back.  This serves a few purposes:
 * 1. During research interviews (with student consent) the session can be recorded for future playback.
 * 2. (possibly) Saving a simulation state for loading later.  Could also be done by exporting state, but that would require extra work.
 * 3. Live broadcasting from one screen to another (sim sharing), for remote learning or projecting a student's sim onto the room's projector
 * 4. Standardize performance testing of a realistic scenario.  For instance, you can record a session and play it back automatically to test scenery performance.
 * 5. Teacher can record sessions for future evaluation.
 * 6. Sim event data collection: when recording logging information for how the user interacts with the UI, also record the model state so it doesn't
 *    have to be recreated in a Finite State Machine afterwards.
 *
 * For recording within a simulation (like Moving Man), another structure would be required.  This is focused on recording the entire session, not
 * just some of the events within a particular model.
 *
 * This implementation just records all state changes, but we could alternatively investigate tracking
 * method calls (like command pattern) or trigger calls, etc.
 *
 * History: Adapted from phetsims/fort/wiretap.js
 * 
 * @author Sam Reid (PhET Interactive Simulations)
 * 
 * TODO: Factor out class into one file, and singleton instance to another file
 * TODO: Remove extra cruft leftover from wiretap.js
 */
define('AXON/log',['require','AXON/axon'], function( require ) {
  

  var axon = require( 'AXON/axon' );
//  var Vector2 = require( 'DOT/Vector2' );

  var cid = 0;

  function Log() {
    var log = this;

    //Enable it if 'log' query parameter specified.  TODO: Switch to has.js?
    //Leave it public so it can be toggled on/off in code
    this.enabled = window && window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'log' );

    //Keep track of all the models, hashed by cid
    //TODO: these could be arrays, we don't need cid on the objects, right?
    this.properties = [];
    this.collections = [];
    this.propertySets = [];

    //Keep track of the changes to all the models
    this.entries = [];

    //Replacer and reviver for the JSON.  Could be moved to the models themselves to decouple/simplify.
    this.replacer = function( key, value ) {

      //Properties must be stored separately, in case of nested properties (such as Forces and Motion: Basics)
      //TODO: A better way of detecting a property?  Perhaps checking the constructor?
      if ( value && value.cid ) {
        return {jsonClass: 'Property', cid: value.cid};
      }

      if ( value && value.constructor.name === 'Vector2' ) {
        return {x: value.x, y: value.y, jsonClass: 'Vector2'};
      }
      return value;
    };

    this.reviver = function( key, value ) {
      if ( value && value.jsonClass && value.jsonClass === 'Property' ) {
        return log.properties[value.cid];
      }
      if ( value && value.jsonClass && value.jsonClass === 'Vector2' ) {
//        return new Vector2( value.x, value.y );
        return {x: value.x, y: value.y };//TODO: pass in a factory that creates Vector2's ?  Log probably shouldn't depend on all of the code that needs reviving
      }
      return value;
    };
  }

  Log.prototype = {

    /**
     * When a property is created, register it for recording and playback.
     * Store its unique cid so that it can be discovered later during playback.
     * @param property
     */
    registerProperty: function( property ) {
      if ( !this.enabled ) {
        return;
      }
      var index = this.properties.length;
      var log = this;
      this.properties.push( property );

      //Don't record initial values for the properties, just the changes.
      property.lazyLink( function( value ) {
        var entry = {time: Date.now(), type: 'property', index: index, action: 'change', value: JSON.stringify( value, log.replacer )};
//        console.log( entry );
        log.entries.push( entry );
      } );
    },
    clear: function() {
      this.properties = [];
      this.collection = [];
      this.propertySets = [];
    },
    stepUntil: function( logArray, playbackTime, logIndex ) {
      var log = this;
      while ( logIndex < logArray.length ) {
        //find any events that passed in this time frame
        //Note, may handle multiple events before calling scene.updateScene()
        var time = logArray[logIndex].time;
        if ( time <= playbackTime ) {
          var entry = logArray[logIndex];
          var cid = entry.cid;

          //if it is a change, then set the value
          if ( entry.action === 'change' ) {
            if ( entry.value ) {
              log.properties[cid].value = JSON.parse( entry.value, log.reviver );
            }
            else {
              console.log( 'missing value for index: ', logIndex, entry );
            }
          }
          else if ( entry.action === 'trigger' ) {
            log.properties[cid].trigger( entry.event );
          }
          else if ( entry.action === 'add' ) {
            log.collections[entry.collectionCid].add( log.properties[entry.cid] );
          }
          else if ( entry.action === 'remove' ) {
            log.collections[entry.collectionCid].remove( log.properties[entry.cid] );
          }
          else if ( entry.action === 'reset' ) {
            log.collections[entry.collectionCid].reset();
          }
          else if ( entry.action === 'sort' ) {
            log.collections[entry.collectionCid].sort();
          }

          logIndex++;
        }
        else {
          break;
        }
      }
      return logIndex;
    }
  };

  axon.log = axon.log || new Log();

  return axon.log;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * An observable property, notifies registered observers when the value changes.
 *
 * Uses the 'Constructor' pattern for object creation, which has the downside that
 * all properties are created once for each instance. It would be nice if our functions
 * were shared. But since the only way to create private fields is in the constructor,
 * and the functions need access to those private fields, there doesn't seem to be
 * any choice but to define the functions in the constructor.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('AXON/Property',['require','AXON/log','AXON/axon'], function( require ) {
  

  var log = require( 'AXON/log' );
  var axon = require( 'AXON/axon' );

  /**
   * @param {*} value
   * @constructor
   */
  axon.Property = function Property( value ) {

    //Store the internal value and the initial value
    this._value = value;
    this._initialValue = value;
    this._observers = [];

    //Set up for recording
    log.registerProperty( this );
  };

  axon.Property.prototype = {

    /**
     * Gets the value.  You can also use the es5 getter (property.value) but this means is provided for inner loops or internal code that must be fast.
     * @return {*}
     */
    get: function() {
      return this._value;
    },

    /**
     * Sets the value and notifies registered observers.  You can also use the es5 getter (property.value) but this means is provided for inner loops or internal code that must be fast.
     * If the value hasn't changed, this is a no-op.
     *
     * @param {*} value
     */
    set: function( value ) {
      if ( value !== this._value ) { //TODO: some Properties probably need deep comparisons here
        var oldValue = this._value;
        this._value = value;
        var observersCopy = this._observers.slice(); // make a copy, in case notification results in removeObserver
        for ( var i = 0; i < observersCopy.length; i++ ) {
          observersCopy[i]( value, oldValue );
        }
      }
    },

    //Use this method when mutating a value (not replacing with a new instance) and you want to send notifications about the change.
    //This is different from the normal axon strategy, but may be necessary to prevent memory allocations.
    //This method is unsafe for removing observers because it assumes the observer list not modified, to save another allocation
    //Only provides the new reference as a callback (no oldvalue)
    //See https://github.com/phetsims/axon/issues/6
    notifyObserversUnsafe: function() {
      for ( var i = 0; i < this._observers.length; i++ ) {
        this._observers[i]( this._value );
      }
    },

    /**
     * Resets the value to the initial value.
     */
    reset: function() {
      this.set( this._initialValue );
    },

    /**
     * This function returns a bound function that sets the specified value.  For use in creating closures e.g. with gui classes.
     * For instance, to have a button that sets a property to true, instead of using
     * button.click(function(){property.set(true);});
     * you could use
     * button.click(property._set(true));
     * @param value the value to use when the setter is called.
     * @return a function that can be used to set the specified value.
     */
    _set: function( value ) {
      return this.set.bind( this, value );
    },

    get value() { return this.get(); },

    set value( newValue ) { this.set( newValue ); },

    /**
     * Adds an observer and notifies it immediately.
     * If observer is already registered, this is a no-op.
     * The initial notification provides the current value for newValue and null for oldValue.
     *
     * @param {Function} observer a function of the form observer(newValue,oldValue)
     */
    link: function( observer ) {
      if ( this._observers.indexOf( observer ) === -1 ) {
        this._observers.push( observer );
        observer( this._value, null ); // null should be used when an object is expected but unavailable
      }
    },

    /**
     * Removes an observer.
     * If observer is not registered, this is a no-op.
     *
     * @param {Function} observer
     */
    unlink: function( observer ) {
      var index = this._observers.indexOf( observer );
      if ( index !== -1 ) {
        this._observers.splice( index, index + 1 );
      }
    },

    /**
     * Add an observer to the Property, without calling it back right away.  This is mostly used for internal code.
     * @param {Function} observer  a function with a single argument, which is the value of the property at the time the function is called.
     */
    lazyLink: function( observer ) {
      if ( this._observers.indexOf( observer ) === -1 ) {
        this._observers.push( observer );
      }
    },

    //Provide toString for console debugging, see http://stackoverflow.com/questions/2485632/valueof-vs-tostring-in-javascript
    toString: function() {return 'Property{' + this.get() + '}'; },
    valueOf: function() {return this.toString();},

    /**
     * Add a listener so that it will only fire once (and not on registration)
     *
     * I can see two ways to implement this:
     * (a) add a field to the observer so after notifications it can be checked and possibly removed. Disadvantage: will make everything slower even if not using 'once'
     * (b) wrap the observer in a new function which will call the observer and then remove itself.  Disadvantage: cannot remove an observer added using 'once'
     * To avoid possible performance problems, use a wrapper function, and return it as a handle in case the 'once' listener must be removed before it is called once
     *
     * @param observer the listener which should be called back only for one property change (and not on registration)
     * @returns {Function} the wrapper handle in case the wrapped function needs to be removed with 'unlink' before it is called once
     */
    once: function( observer ) {
      var property = this;
      var wrapper = function( newValue, oldValue ) {
        observer( newValue, oldValue );
        property.unlink( wrapper );
      };
      this.lazyLink( wrapper );
      return wrapper;
    },

    /**
     * Links an object's named attribute to this property.  Returns a handle so it can be removed.
     * Example: modelVisibleProperty.linkAttribute(view,'visible');
     *
     * @param object
     * @param attributeName
     */
    linkAttribute: function( object, attributeName ) {
      var handle = function( value ) {object[attributeName] = value;};
      this.link( handle );
      return handle;
    },

    /**
     * Returns a new DerivedProperty which is true/false based on whether the value matches (based on ===) the passed in argument.
     * @param value
     * @returns {axon.DerivedProperty}
     */
    valueEquals: function( value ) {
      return new axon.DerivedProperty( [this], function( propertyValue ) { return propertyValue === value; } );
    },

    /**
     * Two way communication for not, so you can set the value and have it come back to the parent
     * Note that noting about the following code is specific to booleans, although this should probably be used mostly for booleans.
     * To unlink both listeners attached unlink a property created with not(), use detach()
     */
    not: function() {
      var parentProperty = this;
      var childProperty = new axon.Property( !this.value );

      var setParentToChild = function( value ) {childProperty.set( !value );};
      parentProperty.link( setParentToChild );

      var setChildToParent = function( value ) {parentProperty.set( !value );};
      childProperty.link( setChildToParent );

      childProperty.detach = function() {
        parentProperty.unlink( setParentToChild );
        childProperty.unlink( setChildToParent );
      };
      return childProperty;
    },

    /**
     * Convenience function for debugging a property values.  It prints the new value on registration and when changed.
     * @param name debug name to be printed on the console
     * @returns {Function} the handle to the linked listener in case it needs to be removed later
     */
    debug: function( name ) {
      var listener = function( value ) { console.log( name, value ); };
      this.link( listener );
      return listener;
    }
  };

  return axon.Property;
} );
// Copyright 2002-2013, University of Colorado

/**
 * Encapsulates common color information and transformations.
 *
 * See http://www.w3.org/TR/css3-color/
 *
 * TODO: make a getHue, getSaturation, getLightness. we can then expose them via ES5!
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/Color',['require','SCENERY/scenery','DOT/Util','DOT/Util'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  var clamp = require( 'DOT/Util' ).clamp;
  var linear = require( 'DOT/Util' ).linear;

  // r,g,b integers 0-255, 'a' float 0-1
  scenery.Color = function Color( r, g, b, a ) {
    
    // allow listeners to be notified on any changes. called with listener()
    this.listeners = [];
    
    if ( typeof r === 'string' ) {
      var str = r.replace( / /g, '' ).toLowerCase();
      var success = false;
      
      // replace colors based on keywords
      var keywordMatch = Color.colorKeywords[str];
      if ( keywordMatch ) {
        str = '#' + keywordMatch;
      }
      
      // run through the available text formats
      for ( var i = 0; i < Color.formatParsers.length; i++ ) {
        var parser = Color.formatParsers[i];
        
        var matches = parser.regexp.exec( str );
        if ( matches ) {
          parser.apply( this, matches );
          success = true;
          break;
        }
      }
      
      if ( !success ) {
        throw new Error( 'scenery.Color unable to parse color string: ' + r );
      }
    } else {
      // alpha
      var alpha = a === undefined ? 1 : a;

      // bitwise handling if 3 elements aren't defined
      if ( g === undefined || b === undefined ) {
        this.setRGBA( ( r >> 16 ) && 0xFF,
                      ( r >> 8 ) && 0xFF,
                      ( r >> 0 ) && 0xFF,
                      alpha );
      }
      else {
        // otherwise, copy them over
        this.setRGBA( r, g, b, alpha );
      }
    }
    
    phetAllocation && phetAllocation( 'Color' );
  };
  var Color = scenery.Color;
  
  // regex utilities
  var rgbNumber = '(-?\\d{1,3}%?)'; // syntax allows negative integers and percentages
  var aNumber = '(\\d+|\\d*\\.\\d+)'; // decimal point number. technically we allow for '255', even though this will be clamped to 1.
  var rawNumber = '(\\d{1,3})'; // a 1-3 digit number
  
  // handles negative and percentage values
  function parseRGBNumber( str ) {
    var multiplier = 1;
    
    // if it's a percentage, strip it off and handle it that way
    if ( str.charAt( str.length - 1 ) === '%' ) {
      multiplier = 2.55;
      str = str.slice( 0, str.length - 1 );
    }
    
    return Math.round( parseInt( str, 10 ) * multiplier );
  }
  
  Color.formatParsers = [
    {
      // 'transparent'
      regexp: /^transparent$/,
      apply: function( color, matches ) {
        color.setRGBA( 0, 0, 0, 0 );
      }
    },{
      // short hex form, a la '#fff'
      regexp: /^#(\w{1})(\w{1})(\w{1})$/,
      apply: function( color, matches ) {
        color.setRGBA( parseInt( matches[1] + matches[1], 16 ),
                       parseInt( matches[2] + matches[2], 16 ),
                       parseInt( matches[3] + matches[3], 16 ),
                       1 );
      }
    },{
      // long hex form, a la '#ffffff'
      regexp: /^#(\w{2})(\w{2})(\w{2})$/,
      apply: function( color, matches ) {
        color.setRGBA( parseInt( matches[1], 16 ),
                       parseInt( matches[2], 16 ),
                       parseInt( matches[3], 16 ),
                       1 );
      }
    },{
      // rgb(...)
      regexp: new RegExp( '^rgb\\(' + rgbNumber + ',' + rgbNumber + ',' + rgbNumber + '\\)$' ),
      apply: function( color, matches ) {
        color.setRGBA( parseRGBNumber( matches[1] ),
                       parseRGBNumber( matches[2] ),
                       parseRGBNumber( matches[3] ),
                       1 );
      }
    },{
      // rgba(...)
      regexp: new RegExp( '^rgba\\(' + rgbNumber + ',' + rgbNumber + ',' + rgbNumber + ',' + aNumber + '\\)$' ),
      apply: function( color, matches ) {
        color.setRGBA( parseRGBNumber( matches[1] ),
                       parseRGBNumber( matches[2] ),
                       parseRGBNumber( matches[3] ),
                       parseFloat( matches[4] ) );
      }
    },{
      // hsl(...)
      regexp: new RegExp( '^hsl\\(' + rawNumber + ',' + rawNumber + '%,' + rawNumber + '%\\)$' ),
      apply: function( color, matches ) {
        color.setHSLA( parseInt( matches[1], 10 ),
                       parseInt( matches[2], 10 ),
                       parseInt( matches[3], 10 ),
                       1 );
      }
    },{
      // hsla(...)
      regexp: new RegExp( '^hsla\\(' + rawNumber + ',' + rawNumber + '%,' + rawNumber + '%,' + aNumber + '\\)$' ),
      apply: function( color, matches ) {
        color.setHSLA( parseInt( matches[1], 10 ),
                       parseInt( matches[2], 10 ),
                       parseInt( matches[3], 10 ),
                       parseFloat( matches[4] ) );
      }
    }
  ];
  
  // see http://www.w3.org/TR/css3-color/
  Color.hueToRGB = function( m1, m2, h ) {
    if ( h < 0 ) {
      h = h + 1;
    }
    if ( h > 1 ) {
      h = h - 1;
    }
    if ( h * 6 < 1 ) {
      return m1 + ( m2 - m1 ) * h * 6;
    }
    if ( h * 2 < 1 ) {
      return m2;
    }
    if ( h * 3 < 2 ) {
      return m1 + ( m2 - m1 ) * ( 2 / 3 - h ) * 6;
    }
    return m1;
  };
  
  Color.prototype = {
    constructor: Color,
    
    copy: function() {
      return new Color( this.r, this.g, this.b, this.a );
    },
    
    // red, integral 0-255
    getRed: function() { return this.r; },
    setRed: function( value ) { return this.setRGBA( value, this.g, this.b, this.a ); },
    get red() { return this.getRed(); },
    set red( value ) { return this.setRed( value ); },
    
    // green, integral 0-255
    getGreen: function() { return this.g; },
    setGreen: function( value ) { return this.setRGBA( this.r, value, this.b, this.a ); },
    get green() { return this.getGreen(); },
    set green( value ) { return this.setGreen( value ); },
    
    // blue, integral 0-255
    getBlue: function() { return this.b; },
    setBlue: function( value ) { return this.setRGBA( this.r, this.g, value, this.a ); },
    get blue() { return this.getBlue(); },
    set blue( value ) { return this.setBlue( value ); },
    
    // alpha, floating 0-1
    getAlpha: function() { return this.a; },
    setAlpha: function( value ) { return this.setRGBA( this.r, this.g, this.b, value ); },
    get alpha() { return this.getAlpha(); },
    set alpha( value ) { return this.setAlpha( value ); },
    
    // RGB integral between 0-255, alpha (float) between 0-1
    setRGBA: function( red, green, blue, alpha ) {
      this.r = Math.round( clamp( red, 0, 255 ) );
      this.g = Math.round( clamp( green, 0, 255 ) );
      this.b = Math.round( clamp( blue, 0, 255 ) );
      this.a = clamp( alpha, 0, 1 );
      
      this.updateColor(); // update the cached value
      
      return this; // allow chaining
    },
    
    computeCSS: function() {
      if ( this.a === 1 ) {
        return 'rgb(' + this.r + ',' + this.g + ',' + this.b + ')';
      } else {
        var alphaString = this.a === 0 || this.a === 1 ? this.a : this.a.toFixed( 20 ); // toFixed prevents scientific notation
        return 'rgba(' + this.r + ',' + this.g + ',' + this.b + ',' + alphaString + ')';
      }
    },
    
    toCSS: function() {
      // verify that the cached value is correct (in debugging builds only, defeats the point of caching otherwise)
      sceneryAssert && sceneryAssert( this._css === this.computeCSS(), 'CSS cached value is ' + this._css + ', but the computed value appears to be ' + this.computeCSS() );
      
      return this._css;
    },
    
    // called to update the interally cached CSS value
    updateColor: function() {
      sceneryAssert && sceneryAssert( !this.immutable, 'Cannot modify an immutable color' );
      
      var oldCSS = this._css;
      this._css = this.computeCSS();
      
      // notify listeners if it changed
      if ( oldCSS !== this._css && this.listeners.length ) {
        var listeners = this.listeners.slice( 0 ); // defensive copy. consider removing if it's a performance bottleneck?
        var length = listeners.length;
        
        for ( var i = 0; i < length; i++ ) {
          listeners[i]();
        }
      }
    },
    
    // allow setting this Color to be immutable when assertions are disabled. any change will throw an error
    setImmutable: function() {
      if ( sceneryAssert ) {
        this.immutable = true;
      }
      
      return this; // allow chaining
    },
    
    // to what value a Canvas's context.fillStyle should be set
    getCanvasStyle: function() {
      return this.toCSS(); // should be inlined, leave like this for future maintainability
    },
    
    // TODO: make a getHue, getSaturation, getLightness. we can then expose them via ES5!
    setHSLA: function( hue, saturation, lightness, alpha ) {
      hue = ( hue % 360 ) / 360;                    // integer modulo 360
      saturation = clamp( saturation / 100, 0, 1 ); // percentage
      lightness = clamp( lightness / 100, 0, 1 );   // percentage
      
      // see http://www.w3.org/TR/css3-color/
      var m1, m2;
      if ( lightness < 0.5 ) {
        m2 = lightness * ( saturation + 1 );
      } else {
        m2 = lightness + saturation - lightness * saturation;
      }
      m1 = lightness * 2 - m2;
      
      this.r = Math.round( Color.hueToRGB( m1, m2, hue + 1/3 ) * 255 );
      this.g = Math.round( Color.hueToRGB( m1, m2, hue ) * 255 );
      this.b = Math.round( Color.hueToRGB( m1, m2, hue - 1/3 ) * 255 );
      this.a = clamp( alpha, 0, 1 );
      
      this.updateColor(); // update the cached value
      
      return this; // allow chaining
    },
    
    equals: function( color ) {
      return this.r === color.r && this.g === color.g && this.b === color.b && this.a === color.a;
    },
    
    withAlpha: function( alpha ) {
      return new Color( this.r, this.g, this.b, alpha );
    },
    
    checkFactor: function( factor ) {
      if ( factor < 0 || factor > 1 ) {
        throw new Error( 'factor must be between 0 and 1: ' + factor );
      }
      return ( factor === undefined ) ? 0.7 : factor;
    },
    
    // matches Java's Color.brighter()
    brighterColor: function( factor ) {
      factor = this.checkFactor( factor );
      var red   = Math.min( 255, Math.floor( this.r / factor ) );
      var green = Math.min( 255, Math.floor( this.g / factor ) );
      var blue  = Math.min( 255, Math.floor( this.b / factor ) );
      return new Color( red, green, blue, this.a );
    },
    
    /**
     * Brightens a color in RGB space. Useful when creating gradients from a
     * single base color.
     *
     * @param color
     * @param factor 0 (no change) to 1 (white)
     * @return lighter (closer to white) version of the original color.
     */
    colorUtilsBrighter: function( factor ) {
      factor = this.checkFactor( factor );
      var red = Math.min( 255, this.getRed() + Math.floor( factor * ( 255 - this.getRed() ) ) );
      var green = Math.min( 255, this.getGreen() + Math.floor( factor * ( 255 - this.getGreen() ) ) );
      var blue = Math.min( 255, this.getBlue() + Math.floor( factor * ( 255 - this.getBlue() ) ) );
      return new Color( red, green, blue, this.getAlpha() );
    },
    
    // matches Java's Color.darker()
    darkerColor: function( factor ) {
      factor = this.checkFactor( factor );
      var red   = Math.max( 0, Math.floor( factor * this.r ) );
      var green = Math.max( 0, Math.floor( factor * this.g ) );
      var blue  = Math.max( 0, Math.floor( factor * this.b ) );
      return new Color( red, green, blue, this.a );
    },
    
    /**
     * Darken a color in RGB space. Useful when creating gradients from a single
     * base color.
     *
     * @param color  the original color
     * @param factor 0 (no change) to 1 (black)
     * @return darker (closer to black) version of the original color.
     */
    colorUtilsDarker: function( factor ) {
      factor = this.checkFactor( factor );
      var red = Math.max( 0, this.getRed() - Math.floor( factor * this.getRed() ) );
      var green = Math.max( 0, this.getGreen() - Math.floor( factor * this.getGreen() ) );
      var blue = Math.max( 0, this.getBlue() - Math.floor( factor * this.getBlue() ) );
      return new Color( red, green, blue, this.getAlpha() );
    },
    
    /*---------------------------------------------------------------------------*
    * listeners TODO: consider mixing in this behavior, it's common
    *----------------------------------------------------------------------------*/
    
    // listener should be a callback expecting no arguments, listener() will be called when the color changes
    addChangeListener: function( listener ) {
      sceneryAssert && sceneryAssert( listener !== undefined && listener !== null, 'Verify that the listener exists' );
      sceneryAssert && sceneryAssert( !_.contains( this.listeners, listener ) );
      this.listeners.push( listener );
    },
    
    removeChangeListener: function( listener ) {
      sceneryAssert && sceneryAssert( _.contains( this.listeners, listener ) );
      this.listeners.splice( _.indexOf( this.listeners, listener ), 1 );
    },

    toString: function() {
      return this.constructor.name + "[r:" + this.r + " g:" + this.g + " b:" + this.b + " a:" + this.a + "]";
    }
  };
  
  Color.basicColorKeywords = {
    aqua:    '00ffff',
    black:   '000000',
    blue:    '0000ff',
    fuchsia: 'ff00ff',
    gray:    '808080',
    green:   '008000',
    lime:    '00ff00',
    maroon:  '800000',
    navy:    '000080',
    olive:   '808000',
    purple:  '800080',
    red:     'ff0000',
    silver:  'c0c0c0',
    teal:    '008080',
    white:   'ffffff',
    yellow:  'ffff00'
  };
  
  Color.colorKeywords = {
    aliceblue:            'f0f8ff',
    antiquewhite:         'faebd7',
    aqua:                 '00ffff',
    aquamarine:           '7fffd4',
    azure:                'f0ffff',
    beige:                'f5f5dc',
    bisque:               'ffe4c4',
    black:                '000000',
    blanchedalmond:       'ffebcd',
    blue:                 '0000ff',
    blueviolet:           '8a2be2',
    brown:                'a52a2a',
    burlywood:            'deb887',
    cadetblue:            '5f9ea0',
    chartreuse:           '7fff00',
    chocolate:            'd2691e',
    coral:                'ff7f50',
    cornflowerblue:       '6495ed',
    cornsilk:             'fff8dc',
    crimson:              'dc143c',
    cyan:                 '00ffff',
    darkblue:             '00008b',
    darkcyan:             '008b8b',
    darkgoldenrod:        'b8860b',
    darkgray:             'a9a9a9',
    darkgreen:            '006400',
    darkkhaki:            'bdb76b',
    darkmagenta:          '8b008b',
    darkolivegreen:       '556b2f',
    darkorange:           'ff8c00',
    darkorchid:           '9932cc',
    darkred:              '8b0000',
    darksalmon:           'e9967a',
    darkseagreen:         '8fbc8f',
    darkslateblue:        '483d8b',
    darkslategray:        '2f4f4f',
    darkturquoise:        '00ced1',
    darkviolet:           '9400d3',
    deeppink:             'ff1493',
    deepskyblue:          '00bfff',
    dimgray:              '696969',
    dodgerblue:           '1e90ff',
    feldspar:             'd19275',
    firebrick:            'b22222',
    floralwhite:          'fffaf0',
    forestgreen:          '228b22',
    fuchsia:              'ff00ff',
    gainsboro:            'dcdcdc',
    ghostwhite:           'f8f8ff',
    gold:                 'ffd700',
    goldenrod:            'daa520',
    gray:                 '808080',
    green:                '008000',
    greenyellow:          'adff2f',
    honeydew:             'f0fff0',
    hotpink:              'ff69b4',
    indianred :           'cd5c5c',
    indigo :              '4b0082',
    ivory:                'fffff0',
    khaki:                'f0e68c',
    lavender:             'e6e6fa',
    lavenderblush:        'fff0f5',
    lawngreen:            '7cfc00',
    lemonchiffon:         'fffacd',
    lightblue:            'add8e6',
    lightcoral:           'f08080',
    lightcyan:            'e0ffff',
    lightgoldenrodyellow: 'fafad2',
    lightgrey:            'd3d3d3',
    lightgreen:           '90ee90',
    lightpink:            'ffb6c1',
    lightsalmon:          'ffa07a',
    lightseagreen:        '20b2aa',
    lightskyblue:         '87cefa',
    lightslateblue:       '8470ff',
    lightslategray:       '778899',
    lightsteelblue:       'b0c4de',
    lightyellow:          'ffffe0',
    lime:                 '00ff00',
    limegreen:            '32cd32',
    linen:                'faf0e6',
    magenta:              'ff00ff',
    maroon:               '800000',
    mediumaquamarine:     '66cdaa',
    mediumblue:           '0000cd',
    mediumorchid:         'ba55d3',
    mediumpurple:         '9370d8',
    mediumseagreen:       '3cb371',
    mediumslateblue:      '7b68ee',
    mediumspringgreen:    '00fa9a',
    mediumturquoise:      '48d1cc',
    mediumvioletred:      'c71585',
    midnightblue:         '191970',
    mintcream:            'f5fffa',
    mistyrose:            'ffe4e1',
    moccasin:             'ffe4b5',
    navajowhite:          'ffdead',
    navy:                 '000080',
    oldlace:              'fdf5e6',
    olive:                '808000',
    olivedrab:            '6b8e23',
    orange:               'ffa500',
    orangered:            'ff4500',
    orchid:               'da70d6',
    palegoldenrod:        'eee8aa',
    palegreen:            '98fb98',
    paleturquoise:        'afeeee',
    palevioletred:        'd87093',
    papayawhip:           'ffefd5',
    peachpuff:            'ffdab9',
    peru:                 'cd853f',
    pink:                 'ffc0cb',
    plum:                 'dda0dd',
    powderblue:           'b0e0e6',
    purple:               '800080',
    red:                  'ff0000',
    rosybrown:            'bc8f8f',
    royalblue:            '4169e1',
    saddlebrown:          '8b4513',
    salmon:               'fa8072',
    sandybrown:           'f4a460',
    seagreen:             '2e8b57',
    seashell:             'fff5ee',
    sienna:               'a0522d',
    silver:               'c0c0c0',
    skyblue:              '87ceeb',
    slateblue:            '6a5acd',
    slategray:            '708090',
    snow:                 'fffafa',
    springgreen:          '00ff7f',
    steelblue:            '4682b4',
    tan:                  'd2b48c',
    teal:                 '008080',
    thistle:              'd8bfd8',
    tomato:               'ff6347',
    turquoise:            '40e0d0',
    violet:               'ee82ee',
    violetred:            'd02090',
    wheat:                'f5deb3',
    white:                'ffffff',
    whitesmoke:           'f5f5f5',
    yellow:               'ffff00',
    yellowgreen:          '9acd32'
  };
  
  // Java compatibility
  Color.BLACK      = new Color( 0,   0,   0   ).setImmutable();
  Color.BLUE       = new Color( 0,   0,   255 ).setImmutable();
  Color.CYAN       = new Color( 0,   255, 255 ).setImmutable();
  Color.DARK_GRAY  = new Color( 64,  64,  64  ).setImmutable();
  Color.GRAY       = new Color( 128, 128, 128 ).setImmutable();
  Color.GREEN      = new Color( 0,   255, 0   ).setImmutable();
  Color.LIGHT_GRAY = new Color( 192, 192, 192 ).setImmutable();
  Color.MAGENTA    = new Color( 255, 0,   255 ).setImmutable();
  Color.ORANGE     = new Color( 255, 200, 0   ).setImmutable();
  Color.PINK       = new Color( 255, 175, 175 ).setImmutable();
  Color.RED        = new Color( 255, 0,   0   ).setImmutable();
  Color.WHITE      = new Color( 255, 255, 255 ).setImmutable();
  Color.YELLOW     = new Color( 255, 255, 0   ).setImmutable();

  /**
   * Interpolates between 2 colors in RGBA space. When distance is 0, color1
   * is returned. When distance is 1, color2 is returned. Other values of
   * distance return a color somewhere between color1 and color2. Each color
   * component is interpolated separately.
   *
   * @param {Color} color1
   * @param {Color} color2
   * @param {Number} distance distance between color1 and color2, 0 <= distance <= 1
   * @return {Color}
   */
  Color.interpolateRBGA = function( color1, color2, distance ) {
    if ( distance < 0 || distance > 1 ) {
      throw new Error( 'distance must be between 0 and 1: ' + distance );
    }
    var r = Math.floor( linear( 0, 1, color1.r, color2.r, distance ) );
    var g = Math.floor( linear( 0, 1, color1.g, color2.g, distance ) );
    var b = Math.floor( linear( 0, 1, color1.b, color2.b, distance ) );
    var a = linear( 0, 1, color1.a, color2.a, distance );
    return new Color( r, g, b, a );
  };

  return Color;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Base class for push buttons that give visual feedback about their state.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/PushButton',['require','SCENERY/input/ButtonListener','PHET_CORE/inherit','SCENERY/nodes/Node','AXON/Property'], function( require ) {
  

  // import
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Property = require( 'AXON/Property' );

  /**
   * @param {Node} upNode
   * @param {Node} overNode
   * @param {Node} downNode
   * @param {Node} disabledNode
   * @param {function} callback
   * @param {object} options
   * @constructor
   */
  function PushButton( upNode, overNode, downNode, disabledNode, callback, options ) {

    options = _.extend( {
        cursor: 'pointer',
        label: 'Button',
        enabled: true
      },
      options );

    var thisButton = this;
    Node.call( thisButton );

    // nodes for each state
    thisButton._upNode = upNode;
    thisButton._overNode = overNode;
    thisButton._downNode = downNode;
    thisButton._disabledNode = disabledNode;

    thisButton._state = 'up';
    thisButton._enabled = new Property( options.enabled );
    thisButton._listeners = [];
    if ( callback ) { thisButton._listeners.push( callback ); }

    thisButton.addChild( upNode );
    thisButton.addChild( overNode );
    thisButton.addChild( downNode );
    thisButton.addChild( disabledNode );

    thisButton.addInputListener( new ButtonListener( {

      up: function() {
        thisButton._state = 'up';
        thisButton._update();
      },

      over: function() {
        thisButton._state = 'over';
        thisButton._update();
      },

      down: function() {
        thisButton._state = 'down';
        thisButton._update();
      },

      out: function() {
        thisButton._state = 'up'; //NOTE: 'out' state looks the same as 'up'
        thisButton._update();
      },

      fire: function() {
        if ( thisButton._enabled.get() ) {
          thisButton._fire();
        }
      }
    } ) );

    // accessibility
    thisButton.addPeer( '<input type="button" aria-label="' + _.escape( options.label ) + '">',
      { click: thisButton._fire.bind( thisButton ) }
    );

    thisButton._enabled.link( function( enabled ) {
      thisButton.cursor = enabled ? options.cursor : 'default';
      thisButton._update();
    } );

    //Mutate with the options after the layout is complete so that you can use width-dependent fields like centerX, etc.
    thisButton.mutate( options );
  }

  return inherit( Node, PushButton, {

    // Adds a listener. If already a listener, this is a no-op.
    addListener: function( listener ) {
      if ( this._listeners.indexOf( listener ) === -1 ) {
        this._listeners.push( listener );
      }
    },

    // Remove a listener. If not a listener, this is a no-op.
    removeListener: function( listener ) {
      var i = this._listeners.indexOf( listener );
      if ( i !== -1 ) {
        this._listeners.splice( i, 1 );
      }
    },

    _fire: function() {
      var copy = this._listeners.slice( 0 );
      copy.forEach( function( listener ) {
        listener();
      } );
    },

    _update: function() {
      // use visible instead of add/removeChild to prevent flickering
      var enabled = this._enabled.get();
      this._upNode.visible = ( this._state === 'up' && enabled );
      this._downNode.visible = ( this._state === 'down' && enabled );
      this._overNode.visible = ( this._state === 'over' && enabled );
      this._disabledNode.visible = !enabled;
    },

    set enabled( value ) { this._enabled.set( value ); },

    get enabled() { return this._enabled.get(); }
  } );
} );

// Copyright 2002-2013, University of Colorado Boulder

//TODO rename this RectanglePushButton?
/**
 * A rectangular button. The background fill changes to give feedback about state. The content node is centered in the rectangle.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/RectangleButton',['require','ASSERT/assert','SCENERY/util/Color','PHET_CORE/inherit','SCENERY/nodes/Node','SUN/PushButton','SCENERY/nodes/Rectangle'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'sun' );
  var Color = require( 'SCENERY/util/Color' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var PushButton = require( 'SUN/PushButton' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );

  // Creates a node that represents the button is a specific state.
  var createNode = function( content, stroke, fill, rectangleLineWidth, rectangleXMargin, rectangleYMargin, rectangleCornerRadius ) {
    var node = new Node();
    var rectangle = new Rectangle(
      0, 0, content.width + rectangleXMargin + rectangleXMargin, content.height + rectangleYMargin + rectangleYMargin, rectangleCornerRadius, rectangleCornerRadius,
      { stroke: stroke, fill: fill, rectangleLineWidth: rectangleLineWidth } );
    node.addChild( rectangle );
    node.addChild( content );
    content.centerX = rectangle.centerX;
    content.centerY = rectangle.centerY;
    return node;
  };

  /**
   * @param {Node} content
   * @param {Function} callback
   * @param options
   * @constructor
   */
  function RectangleButton( content, callback, options ) {

    //TODO it would be nice to have less-verbose names, but supertypes may have similar options. For example, TextButton has textFill.
    options = _.extend( {
        // stroke
        rectangleStroke: 'black',
        rectangleStrokeDisabled: 'rgb(175,175,175)',
        // fill
        rectangleFillUp: new Color( 255, 200, 0 ),
        rectangleFillDisabled: 'white',
        // options that apply to all states
        rectangleLineWidth: 1,
        rectangleXMargin: 5,
        rectangleYMargin: 5,
        rectangleCornerRadius: 10
      },
      options );

    // generated colors
    assert && assert( options.rectangleFillUp instanceof Color ); //TODO can we relax this requirement?
    options.rectangleFillOver = options.rectangleFillOver || options.rectangleFillUp.brighterColor( 0.9 );
    options.rectangleFillDown = options.rectangleFillDown || options.rectangleFillUp.darkerColor( 0.9 );

    PushButton.call( this,
      createNode( content, options.rectangleStroke, options.rectangleFillUp, options.rectangleLineWidth, options.rectangleXMargin, options.rectangleYMargin, options.rectangleCornerRadius ),
      createNode( content, options.rectangleStroke, options.rectangleFillOver, options.rectangleLineWidth, options.rectangleXMargin, options.rectangleYMargin, options.rectangleCornerRadius ),
      createNode( content, options.rectangleStroke, options.rectangleFillDown, options.rectangleLineWidth, options.rectangleXMargin, options.rectangleYMargin, options.rectangleCornerRadius ),
      createNode( content, options.rectangleStrokeDisabled, options.rectangleFillDisabled, options.rectangleLineWidth, options.rectangleXMargin, options.rectangleYMargin, options.rectangleCornerRadius ),
      callback, options );
  }

  return inherit( PushButton, RectangleButton );
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Button with text on a rectangle.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/TextButton',['require','PHET_CORE/inherit','SUN/RectangleButton','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var RectangleButton = require( 'SUN/RectangleButton' );
  var Text = require( 'SCENERY/nodes/Text' );

  function TextButton( text, callback, options ) {

    options = _.extend( {
      textFill: 'black',
      textFillDisabled: 'rgb(175,175,175)'
    }, options );

    //TODO sun#24, apply options to both TextButton and Text will cause problems for options like x,y,scale,...
    var textNode = new Text( text, options );

    RectangleButton.call( this, textNode, callback, options );

    // enable/disable the pieces that are specific to this subtype
    this._enabled.link( function( enabled ) {
      textNode.fill = enabled ? options.textFill : options.textFillDisabled;
    } );
  }

  inherit( RectangleButton, TextButton );

  return TextButton;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Shows the about dialog.
 *
 * @author Sam Reid
 */
define('JOIST/SettingsDialog',['require','SCENERY/nodes/VBox','SCENERY/nodes/Rectangle','SCENERY/nodes/Text','PHET_CORE/inherit','JOIST/ScreenView','SUN/Panel','SUN/CheckBox','SCENERY_PHET/PhetFont','AXON/Property','SUN/TextButton'], function( require ) {
  

  var VBox = require( 'SCENERY/nodes/VBox' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Text = require( 'SCENERY/nodes/Text' );
  var inherit = require( 'PHET_CORE/inherit' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var Panel = require( 'SUN/Panel' );
  var CheckBox = require( 'SUN/CheckBox' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var TextButton = require( 'SUN/TextButton' );

  /**
   * @param {Sim} sim
   * @constructor
   */
  function SettingsDialog( sim ) {
    var settingsDialog = this;

    //Use view, to help center and scale content
    ScreenView.call( this, {renderer: 'svg'} );

    var content = new VBox( { align: 'center', spacing: 50, children: [
      new Text( 'Settings', { font: new PhetFont( 16 ) } ),
      new CheckBox( new Text( 'Show Pointers' ), sim.showPointersProperty ),
      new TextButton( 'Done', function() {
        settingsDialog.doneListeners.forEach( function( listener ) {
          listener();
        } );
      } )
    ]} );

    //Show a gray overlay that will help focus on the about dialog, and prevent clicks on the sim while the dialog is up
    this.addChild( new Panel( content, {centerX: this.layoutBounds.centerX, centerY: this.layoutBounds.centerY, xMargin: 20, yMargin: 20 } ) );

    function resize() {
      settingsDialog.layout( $( window ).width(), $( window ).height() );
    }

    //Fit to the window and render the initial scene
    $( window ).resize( resize );
    resize();
    this.doneListeners = [];
  }

  return inherit( ScreenView, SettingsDialog, {
    addDoneListener: function( listener ) {
      this.doneListeners.push( listener );
    }
  } );
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * A node which always fills the entire screen, no matter what the transform is.
 * Used for showing an overlay on the screen e.g., when a popup dialog is shown.
 * This can fade the background to focus on the dialog/popup as well as intercept mouse events for dismissing the dialog/popup.
 * Note: This is currently implemented using large numbers, it should be rewritten to work in any coordinate frame, possibly using kite.Shape.plane()
 * TODO: Implement using infinite geometry
 *
 * @author Sam Reid
 */

define('SCENERY/nodes/Plane',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/nodes/Rectangle','KITE/Shape'], function( require ) {
  

  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );

  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );

  /**
   * @param options Any options for the plane, typically fill/opacity (many others such as scale, translation, rotation should have no effect)
   * @constructor
   */
  scenery.Plane = function Plane( options ) {
    Rectangle.call( this, -2000, -2000, 6000, 6000, options );
  };
  var Plane = scenery.Plane;

  return inherit( Rectangle, Plane );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * The 'PhET' menu.
 *
 * @author Sam Reid
 */
define('JOIST/PhetMenu',['require','SCENERY/nodes/Node','KITE/Shape','SCENERY/nodes/Path','SCENERY/nodes/Text','PHET_CORE/inherit','JOIST/AboutDialog','JOIST/SettingsDialog','SCENERY/nodes/Rectangle','SCENERY/input/ButtonListener','SCENERY/nodes/Plane','AXON/log','SCENERY_PHET/PhetFont'], function( require ) {
  

  // imports
  var Node = require( 'SCENERY/nodes/Node' );
  var Shape = require( 'KITE/Shape' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Text = require( 'SCENERY/nodes/Text' );
  var inherit = require( 'PHET_CORE/inherit' );
  var AboutDialog = require( 'JOIST/AboutDialog' );
  var SettingsDialog = require( 'JOIST/SettingsDialog' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Plane = require( 'SCENERY/nodes/Plane' );
  var log = require( 'AXON/log' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );

  // constants
  var FONT_SIZE = 18;
  var HIGHLIGHT_COLOR = '#a6d2f4';

  // Creates a menu item that highlights and fires.
  var createMenuItem = function( text, width, height, callback, immediateCallback ) {

    var X_MARGIN = 5;
    var Y_MARGIN = 3;
    var CORNER_RADIUS = 5;

    var textNode = new Text( text, { font: new PhetFont( FONT_SIZE ) } );
    var highlight = new Rectangle( 0, 0, width + X_MARGIN + X_MARGIN, height + Y_MARGIN + Y_MARGIN, CORNER_RADIUS, CORNER_RADIUS );

    var menuItem = new Node( { cursor: 'pointer' } );
    menuItem.addChild( highlight );
    menuItem.addChild( textNode );

    textNode.left = highlight.left + X_MARGIN; // text is left aligned
    textNode.centerY = highlight.centerY;

    menuItem.addInputListener( {
      enter: function() { highlight.fill = HIGHLIGHT_COLOR; },
      exit: function() { highlight.fill = null; },
      upImmediate: function() { immediateCallback && immediateCallback(); }
    } );
    menuItem.addInputListener( new ButtonListener( {fire: callback } ) );

    return menuItem;
  };

  // Creates a comic-book style bubble.
  var createBubble = function( width, height ) {

    var rectangle = new Rectangle( 0, 0, width, height, 8, 8, {fill: 'white', lineWidth: 1, stroke: 'black'} );

    var tail = new Shape();
    tail.moveTo( width - 20, height - 2 );
    tail.lineToRelative( 0, 20 );
    tail.lineToRelative( -20, -20 );
    tail.close();

    var tailOutline = new Shape();
    tailOutline.moveTo( width - 20, height );
    tailOutline.lineToRelative( 0, 20 - 2 );
    tailOutline.lineToRelative( -18, -18 );

    var bubble = new Node();
    bubble.addChild( rectangle );
    bubble.addChild( new Path( tail, {fill: 'white'} ) );
    bubble.addChild( new Path( tailOutline, {stroke: 'black', lineWidth: 1} ) );

    return bubble;
  };

  //TODO: The popup menu should scale with the size of the screen
  function PhetMenu( sim, options ) {

    options = _.extend( {renderer: 'svg'}, options );

    var thisMenu = this;
    Node.call( thisMenu );

    /*
     * Description of the items in the menu. Each descriptor has these properties:
     * {String} text - the item's text
     * {Boolean} present - whether the item should be added to the menu
     * {Function} callback - called when the item fires
     */
    var itemDescriptors = [
      {
        text: 'PhET website',
        present: true,
        callback: function() {
        },
        immediateCallback: function() {
          var phetWindow = window.open( 'http://phet.colorado.edu', '_blank' );
          phetWindow.focus();
        } },
      {
        text: 'Output Log',
        present: log.enabled ? true : false, // because double-negation (!!) coercion doesn't look as cool?
        callback: function() {
          console.log( JSON.stringify( log.entries ) );
        }},
      {
        text: 'Output Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
          // prints the recorded input event log to the console
          console.log( sim.getRecordedInputEventLogString() );
        }},
      {
        text: 'Submit Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
          // submits a recorded event log to the same-origin server (run scenery/tests/event-logs/server/server.js with Node, from the same directory)
          sim.submitEventLog();
        }},
      {
        text: 'Mail Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
        },
        immediateCallback: function() {
          // mailto: link including the body to email
          sim.mailEventLog();
        }},
//      {
//        text: 'Settings...',
//        present: true,
//        callback: function() {
//          var settingsDialog = new SettingsDialog( sim );
//          var plane = new Plane( {fill: 'black', opacity: 0.3} );
//          sim.addChild( plane );
//          sim.addChild( settingsDialog );
//          settingsDialog.addDoneListener( function() {
//            plane.detach();
//            settingsDialog.detach();
//          } );
//        }
//      },
      {
        text: 'About...',
        present: true,
        callback: function() {
          var aboutDialog = new AboutDialog( sim );
          var plane = new Plane( {fill: 'black', opacity: 0.3} );
          sim.addChild( plane );
          sim.addChild( aboutDialog );
          var aboutDialogListener = {up: function() {
            aboutDialog.removeInputListener( aboutDialogListener );
            plane.addInputListener( aboutDialogListener );
            aboutDialog.detach();
            plane.detach();
          }};
          aboutDialog.addInputListener( aboutDialogListener );
          plane.addInputListener( aboutDialogListener );
        }}
    ];

    // Menu items have uniform size, so compute the max text dimensions.
    var keepItemDescriptors = _.filter( itemDescriptors, function( itemDescriptor ) {return itemDescriptor.present;} );
    var textNodes = _.map( keepItemDescriptors, function( item ) {return new Text( item.text, {font: new PhetFont( FONT_SIZE )} );} );
    var maxTextWidth = _.max( textNodes,function( node ) {return node.width;} ).width;
    var maxTextHeight = _.max( textNodes,function( node ) {return node.height;} ).height;

    // Create the menu items.
    var items = _.map( keepItemDescriptors, function( itemDescriptor ) {
      return createMenuItem( itemDescriptor.text, maxTextWidth, maxTextHeight, itemDescriptor.callback, itemDescriptor.immediateCallback );
    } );

    // Create a comic-book-style bubble.
    var itemWidth = _.max( items,function( item ) {return item.width;} ).width;
    var itemHeight = _.max( items,function( item ) {return item.height;} ).height;
    var X_MARGIN = 5;
    var Y_MARGIN = 5;
    var bubbleWidth = itemWidth + X_MARGIN + X_MARGIN;
    var bubbleHeight = itemHeight * items.length + Y_MARGIN + Y_MARGIN;
    thisMenu.addChild( createBubble( bubbleWidth, bubbleHeight ) );

    // Populate the bubble with menu items.
    var y = Y_MARGIN;
    _.each( items, function( item ) {
      item.top = y;
      item.left = X_MARGIN;
      thisMenu.addChild( item );

      //TODO separators should be specified in itemDescriptors
      // Put a separator before the last item.
      if ( item === items[items.length - 2] ) {
        thisMenu.addChild( new Path( Shape.lineSegment( 8, y + itemHeight, bubbleWidth - 8, y + itemHeight ), {stroke: 'gray', lineWidth: 1} ) );
      }
      y += itemHeight;
    } );

    thisMenu.mutate( options );
  }

  inherit( Node, PhetMenu );

  return PhetMenu;
} );

// Copyright 2002-2013, University of Colorado

/**
 * Gradient base type for LinearGradient and RadialGradient. Will not function on its own
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/Gradient',['require','SCENERY/util/Color','SCENERY/scenery','DOT/Vector2'], function( require ) {
  
  
  require( 'SCENERY/util/Color' );
  var scenery = require( 'SCENERY/scenery' );
  
  var Vector2 = require( 'DOT/Vector2' );

  // TODO: add the ability to specify the color-stops inline. possibly [ [0,color1], [0.5,color2], [1,color3] ]
  scenery.Gradient = function Gradient( canvasGradient ) {
    sceneryAssert && sceneryAssert( this.constructor.name !== 'Gradient', 'Please create a LinearGradient or RadialGradient. Do not directly use the supertype Gradient.' );
    
    this.stops = [];
    this.lastStopRatio = 0;
    
    this.canvasGradient = canvasGradient;
    
    this.transformMatrix = null;
  };
  var Gradient = scenery.Gradient;
  
  Gradient.prototype = {
    constructor: Gradient,
    
    /**
     * @param {Number} ratio        Monotonically increasing value in the range of 0 to 1
     * @param {Color|String} color  Color for the stop, either a scenery.Color or CSS color string
     */
    addColorStop: function( ratio, color ) {
      // TODO: invalidate the gradient?
      if ( this.lastStopRatio > ratio ) {
        // fail out, since browser quirks go crazy for this case
        throw new Error( 'Color stops not specified in the order of increasing ratios' );
      } else {
        this.lastStopRatio = ratio;
      }
      
      // make sure we have a scenery.Color now
      if ( typeof color === 'string' ) {
        color = new scenery.Color( color );
      }
      
      this.stops.push( {
        ratio: ratio,
        color: color
      } );
      
      // construct the Canvas gradient as we go
      this.canvasGradient.addColorStop( ratio, color.toCSS() );
      return this;
    },
    
    setTransformMatrix: function( transformMatrix ) {
      // TODO: invalidate the gradient?
      if ( this.transformMatrix !== transformMatrix ) {
        this.transformMatrix = transformMatrix;
      }
      return this;
    },
    
    getCanvasStyle: function() {
      return this.canvasGradient;
    }
  };
  
  return Gradient;
} );

// Copyright 2002-2013, University of Colorado

/**
 * A linear gradient that can be passed into the 'fill' or 'stroke' parameters.
 *
 * SVG gradients, see http://www.w3.org/TR/SVG/pservers.html
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/util/LinearGradient',['require','SCENERY/scenery','PHET_CORE/inherit','DOT/Vector2','SCENERY/util/Gradient'], function( require ) {
  
  
  var scenery = require( 'SCENERY/scenery' );
  
  var inherit = require( 'PHET_CORE/inherit' );
  var Vector2 = require( 'DOT/Vector2' );
  var Gradient = require( 'SCENERY/util/Gradient' );

  // TODO: add the ability to specify the color-stops inline. possibly [ [0,color1], [0.5,color2], [1,color3] ]
  scenery.LinearGradient = function LinearGradient( x0, y0, x1, y1 ) {
    sceneryAssert && sceneryAssert( isFinite( x0 ) && isFinite( y0 ) && isFinite( x1 ) && isFinite( y1 ) );
    var usesVectors = y1 === undefined;
    if ( usesVectors ) {
      sceneryAssert && sceneryAssert( ( x0 instanceof Vector2 ) && ( y0 instanceof Vector2 ), 'If less than 4 parameters are given, the first two parameters must be Vector2' );
    }
    this.start = usesVectors ? x0 : new Vector2( x0, y0 );
    this.end = usesVectors ? y0 : new Vector2( x1, y1 );
    
    // use the global scratch canvas instead of creating a new Canvas
    Gradient.call( this, scenery.scratchContext.createLinearGradient( x0, y0, x1, y1 ) );
  };
  var LinearGradient = scenery.LinearGradient;
  
  inherit( Gradient, LinearGradient, {
    
    // seems we need the defs: http://stackoverflow.com/questions/7614209/linear-gradients-in-svg-without-defs
    // SVG: spreadMethod 'pad' 'reflect' 'repeat' - find Canvas usage
    getSVGDefinition: function( id ) {
      /* Approximate example of what we are creating:
      <linearGradient id="grad2" x1="0" y1="0" x2="100" y2="0" gradientUnits="userSpaceOnUse">
        <stop offset="0" style="stop-color:rgb(255,255,0);stop-opacity:1" />
        <stop offset="0.5" style="stop-color:rgba(255,255,0,0);stop-opacity:0" />
        <stop offset="1" style="stop-color:rgb(255,0,0);stop-opacity:1" />
      </linearGradient>
      */
      var svgns = 'http://www.w3.org/2000/svg'; // TODO: store this in a common place!
      var definition = document.createElementNS( svgns, 'linearGradient' );
      definition.setAttribute( 'id', id );
      definition.setAttribute( 'gradientUnits', 'userSpaceOnUse' ); // so we don't depend on the bounds of the object being drawn with the gradient
      definition.setAttribute( 'x1', this.start.x );
      definition.setAttribute( 'y1', this.start.y );
      definition.setAttribute( 'x2', this.end.x );
      definition.setAttribute( 'y2', this.end.y );
      if ( this.transformMatrix ) {
        definition.setAttribute( 'gradientTransform', this.transformMatrix.getSVGTransform() );
      }
      
      _.each( this.stops, function( stop ) {
        var stopElement = document.createElementNS( svgns, 'stop' );
        stopElement.setAttribute( 'offset', stop.ratio );
        stopElement.setAttribute( 'style', 'stop-color: ' + stop.color.withAlpha( 1 ).toCSS() + '; stop-opacity: ' + stop.color.a.toFixed( 20 ) + ';' );
        definition.appendChild( stopElement );
      } );
      
      return definition;
    },
    
    toString: function() {
      var result = 'new scenery.LinearGradient( ' + this.start.x + ', ' + this.start.y + ', ' + this.end.x + ', ' + this.end.y + ' )';
      
      _.each( this.stops, function( stop ) {
        result += '.addColorStop( ' + stop.ratio + ', \'' + stop.color.toString() + '\' )';
      } );
      
      return result;
    }
  } );
  
  return LinearGradient;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Code for testing which platform is running.  Use sparingly, if at all!
 *
 * Sample usage:
 * if (platform.firefox) {node.renderer = 'canvas';}
 *
 * @author Sam Reid
 */
define('PHET_CORE/platform',['require'], function( require ) {
  
  
  // taken from HomeScreen
  function isIE( version ) {
    var r = new RegExp( 'msie' + (!isNaN( version ) ? ('\\s' + version) : ''), 'i' );
    return r.test( navigator.userAgent );
  }
  
  return {
    get firefox() { return navigator.userAgent.toLowerCase().indexOf( 'firefox' ) > -1; },

    //see http://stackoverflow.com/questions/3007480/determine-if-user-navigated-from-mobile-safari
    get mobileSafari() { return navigator.userAgent.match( /(iPod|iPhone|iPad)/ ) && navigator.userAgent.match( /AppleWebKit/ ); },
    
    get ie9() { return isIE( 9 ); },
    get ie10() { return isIE( 10 ); },
    get ie() { return navigator.userAgent.indexOf( 'MSIE' ) !== -1; },
    
    // from HomeScreen
    get android() { return navigator.userAgent.indexOf( 'Android' ) > 0; }
  };
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Image plugin that loads an image dynamically from the file system at development time, but from base64 content after a build.
 * For development time, this is pretty similar to the image plugin at https://github.com/millermedeiros/requirejs-plugins
 *
 * //TODO: exclude the plugin itself from build file.  RequireJS docs said there is an easy way to do so
 * @author Sam Reid
 */
define('image',[], function() {

  //Keep track of the images that are used during dependency resolution so they can be converted to base64 at compile time
  var buildMap = {};

  //This function was taken from chipper/grunt/Gruntfile.js
  var loadFileAsDataURI = function( filename ) {
    var mimeType = {
      'png': 'image/png',
      'svg': 'image/svg+xml',
      'jpg': 'image/jpeg',
      'cur': 'image/x-icon', // cursor files (used in build-a-molecule). x-win-bitmap gives off warnings in Chrome
      'mp3': 'audio/mpeg',
      'm4a': 'audio/mp4',
      'ogg': 'audio/ogg',
      'oga': 'audio/ogg',
      'bma': 'audio/webm', // webma is the full extension
      'wav': 'audio/wav'
    }[filename.slice( -3 )];

    //TODO: error check the mimeType
//    assert( mimeType, 'Unknown mime type for filename: ' + filename );

    var base64 = 'data:' + mimeType + ';base64,' + Buffer( fs.readFileSync( filename ) ).toString( 'base64' );
    return base64;
  };

  return {
    load: function( name, parentRequire, onload, config ) {
      if ( config.isBuild ) {
        buildMap[name] = parentRequire.toUrl( name );
        onload( null );
      }
      else {
        var image = new Image();
        image.onerror = function( error ) { onload.error( error ); };
        image.onload = function() {
          onload( image );

          //See https://github.com/millermedeiros/requirejs-plugins/blob/master/src/json.js
          buildMap[name] = parentRequire.toUrl( name );
          delete image.onload;
        };
        image.src = parentRequire.toUrl( name );
      }
    },

    //write method based on RequireJS official text plugin by James Burke
    //https://github.com/jrburke/requirejs/blob/master/text.js
    write: function( pluginName, moduleName, write ) {
      if ( moduleName in buildMap ) {
        var content = buildMap[moduleName];

        var base64 = loadFileAsDataURI( content );

        //Write code that will load the image and register with a global `phetImages` to make sure everything loaded, see SimLauncher.js
        write( 'define("' + pluginName + '!' + moduleName + '", function(){ ' +
               'var img = new Image();\n' +
               'window.phetImages = window.phetImages || []\n' +
               'window.phetImages.push(img);\n' +
               'img.src=\'' + base64 + '\';\n' +
               'return img;});\n' );
      }
    }

  };
} );
define("image!JOIST/../images/phet-logo-short.svg", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjEsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4KCjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICBpZD0iTGF5ZXJfMiIKICAgeD0iMHB4IgogICB5PSIwcHgiCiAgIHdpZHRoPSIyNzMuMTM2OTkiCiAgIGhlaWdodD0iMTA4LjExOCIKICAgdmlld0JveD0iMCAwIDI3My4xMzY5OSAxMDguMTE4IgogICBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA1MDQgMzYwIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ4LjQgcjk5MzkiCiAgIHNvZGlwb2RpOmRvY25hbWU9InBoZXQtbG9nby1zaG9ydC5zdmciPjxtZXRhZGF0YQogICBpZD0ibWV0YWRhdGEzMDA4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+PC9jYzpXb3JrPjwvcmRmOlJERj48L21ldGFkYXRhPjxkZWZzCiAgIGlkPSJkZWZzMzAwNiIgLz48c29kaXBvZGk6bmFtZWR2aWV3CiAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgIG9iamVjdHRvbGVyYW5jZT0iMTAiCiAgIGdyaWR0b2xlcmFuY2U9IjEwIgogICBndWlkZXRvbGVyYW5jZT0iMTAiCiAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjY0MCIKICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNDgwIgogICBpZD0ibmFtZWR2aWV3MzAwNCIKICAgc2hvd2dyaWQ9ImZhbHNlIgogICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgZml0LW1hcmdpbi1sZWZ0PSIwIgogICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIKICAgaW5rc2NhcGU6em9vbT0iMS42Mjc3Nzc4IgogICBpbmtzY2FwZTpjeD0iMTM2LjU2OCIKICAgaW5rc2NhcGU6Y3k9IjU0LjA1OSIKICAgaW5rc2NhcGU6d2luZG93LXg9IjAiCiAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIwIgogICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJMYXllcl8yIiAvPgo8ZwogICBpZD0iZzI5ODYiCiAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMTUuNDMyLC0xMjUuOTQxKSI+Cgk8ZwogICBpZD0iZzI5ODgiPgoJCTxwYXRoCiAgIGQ9Im0gMTQ5LjQzMiwxNTAuMDcgaCAtMzIgYyAtMS4xMDQsMCAtMiwwLjg5NiAtMiwyIHYgNzkuOTg4IGMgMCwxLjEwNCAwLjg5NiwyIDIsMiBoIDE3LjY2IGMgMS4xMDQsMCAyLC0wLjg5NiAyLC0yIHYgLTIwLjkzIGMgMCwtMS4xMDUgMC44OTUsLTIgMiwtMiBoIDEwLjM0IGMgMjAuMzk4LDAgMzIuMTA5LC0xMC44MyAzMi4xMDksLTI5LjU5MiAwLC0xOC42MzggLTExLjcxMSwtMjkuNDY2IC0zMi4xMDksLTI5LjQ2NiB6IG0gLTIuMDE1LDQxLjE3NiBoIC04LjMyNiBjIC0xLjEwNSwwIC0yLC0wLjg5NiAtMiwtMiB2IC0xOS41NDcgYyAwLC0xLjEwNSAwLjg5NSwtMiAyLC0yIGggOC4zMjYgYyA5LjU2OCwwIDEyLjU5LDYuNTQ3IDEyLjU5LDExLjgzOCAwLDUuNTQxIC0zLjAyMiwxMS43MDkgLTEyLjU5LDExLjcwOSB6IgogICBpZD0icGF0aDI5OTAiCiAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgIHN0eWxlPSJmaWxsOiM2YWNlZjUiIC8+CgkJPHBhdGgKICAgZD0ibSAzODYuNTY4LDE1MC4wNyBoIC0xMjIuODEgYyAtMS4xMDQsMCAtMiwwLjg5NiAtMiwyIHYgNzkuOTg4IGMgMCwxLjEwNCAwLjg5NiwyIDIsMiBoIDU0LjU1NiBjIDEuMTA0LDAgMiwtMC44OTYgMiwtMiB2IC0xMy43NTQgYyAwLC0xLjEwNSAtMC44OTYsLTIgLTIsLTIgaCAtMzMuODk2IGMgLTAuNTUzLDAgLTEsLTAuNDQ3IC0xLC0xIHYgLTEzLjM2MyBjIDAsLTAuNTUzIDAuNDQ3LC0xIDEsLTEgSCAzMTMuNCBjIDEuMTA0LDAgMiwtMC44OTYgMiwtMiB2IC0xMy43NTQgYyAwLC0xLjEwNCAtMC44OTYsLTIgLTIsLTIgaCAtMjguOTgxIGMgLTAuNTUzLDAgLTEsLTAuNDQ4IC0xLC0xIHYgLTEzLjQ4OSBjIDAsLTAuNTUyIDAuNDQ3LC0xIDEsLTEgbCA1Ny4wNTcsMC4wNjMgYyAwLjU1MywwIDEsMC40NDkgMSwxIHYgNjMuMjk3IGMgMCwxLjEwNCAwLjg5NiwyIDIsMiBoIDE3LjY2IGMgMS4xMDQsMCAyLC0wLjg5NiAyLC0yIHYgLTYzLjI5NyBjIDAsLTAuNTUxIDAuNDQ3LC0xIDEsLTEgaCAyMS40MzMgYyAxLjEwNCwwIDIsLTAuODk2IDIsLTIgViAxNTIuMDcgYyAtMC4wMDEsLTEuMTAzIC0wLjg5NiwtMiAtMi4wMDEsLTIgeiIKICAgaWQ9InBhdGgyOTkyIgogICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICBzdHlsZT0iZmlsbDojNmFjZWY1IiAvPgoJPC9nPgoJPGcKICAgaWQ9ImcyOTk0Ij4KCQk8cGF0aAogICBkPSJtIDIzNS40ODgsMTcyLjc0MiBjIC0wLjc5OSwyLjQ4OCAtMS41NTcsNS4wMDQgLTIuMjQ0LDcuNTY5IC0wLjE0NSwwLjUzOSAtMC4yODMsMS4wOTEgLTAuNDIzLDEuNjQgLTYuNDM2LDI3LjgxMiAtMi45NDYsNDUuOTk4IC0xLjczNSw1MS45MTkgMC4yODEsMC4xMTggMC41OCwwLjE4OCAwLjg4MiwwLjE4OCBoIDE2LjMzMSBjIDEuMTA1LDAgMiwtMC44OTYgMiwtMiB2IC0zNS4wMiBjIDAsLTExLjYwNyAtNS4yNTIsLTIwLjMxOCAtMTQuODExLC0yNC4yOTYgeiIKICAgaWQ9InBhdGgyOTk2IgogICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICBzdHlsZT0iZmlsbDojZjJlOTE2IiAvPgoJCTxwYXRoCiAgIGQ9Ik0gMjExLjc2NywxNzEuMDk5IFYgMTQ5LjMgYyAwLC0xLjEwNCAtMC44OTYsLTIgLTIsLTIgaCAtMTcuNjU4IGMgLTEuMTA0LDAgLTIsMC44OTYgLTIsMiB2IDgyLjc1OSBjIDAsMS4xMDQgMC44OTYsMiAyLDIgaCAxMS4xMDkgYyAwLjUwNCwwIDAuOTkxLC0wLjE5MSAxLjM5MywtMC40OTUgMy4xMzEsLTI1LjA3MSAxMi40OTcsLTQ2Ljg4NCAyMS43NSwtNjMuMDMyIC0xLjI1NCwtMC4xMTkgLTIuNTQ4LC0wLjE4OCAtMy44OTEsLTAuMTg4IC0zLjQsLTAuMDAxIC03LjY4MiwwLjI0OSAtMTAuNzAzLDAuNzU1IHoiCiAgIGlkPSJwYXRoMjk5OCIKICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgc3R5bGU9ImZpbGw6I2YyZTkxNiIgLz4KCQk8cG9seWdvbgogICBwb2ludHM9IjIzNy45NzksMTQ3LjkyMiAyNTEuMjMxLDE1Mi41OTQgMjU2LjAxNywxMjUuOTQxIDIyMS45MTYsMTQyLjE5MSAyMzQuMzAyLDE0Ni42NzIgMjUwLjI0OCwxMzMuMzk1ICIKICAgaWQ9InBvbHlnb24zMDAwIgogICBzdHlsZT0iZmlsbDojZjJlOTE2IiAvPgoJCTxwb2x5Z29uCiAgIHBvaW50cz0iMjM3Ljk5OSwxNTAuNTE0IDIzOC4zMDcsMTU3LjQ0NyAyNDMuMjQ5LDE1Mi41MDcgIgogICBpZD0icG9seWdvbjMwMDIiCiAgIHN0eWxlPSJmaWxsOiNmMmU5MTYiIC8+Cgk8L2c+CjwvZz4KPC9zdmc+';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * The button that pops up the PhET menu.
 */
define('JOIST/PhetButton',['require','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY/nodes/Text','SCENERY/nodes/Image','SUN/FontAwesomeNode','SCENERY/nodes/Rectangle','SCENERY/nodes/Plane','PHET_CORE/inherit','PHET_CORE/platform','JOIST/PhetMenu','SCENERY/util/Font','KITE/Shape','SCENERY/util/LinearGradient','SCENERY/input/ButtonListener','DOT/Vector2','image!JOIST/../images/phet-logo-short.svg'], function( require ) {
  

  //imports
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Image = require( 'SCENERY/nodes/Image' );
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Plane = require( 'SCENERY/nodes/Plane' );
  var inherit = require( 'PHET_CORE/inherit' );
  var platform = require( 'PHET_CORE/platform' );
  var PhetMenu = require( 'JOIST/PhetMenu' );
  var Font = require( 'SCENERY/util/Font' );
  var Shape = require( 'KITE/Shape' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Vector2 = require( 'DOT/Vector2' );
  var phetLogo = require( 'image!JOIST/../images/phet-logo-short.svg' );

  //TODO this is copied from NavigationBar
  var createHighlight = function( width, height ) {
    var leftBar = new Path( Shape.lineSegment( 0, 0, 0, height ), {lineWidth: 1, stroke: new LinearGradient( 0, 0, 0, height ).addColorStop( 0, 'black' ).addColorStop( 0.5, 'white' ).addColorStop( 1, 'black' ) } );
    var rightBar = new Path( Shape.lineSegment( width, 0, width, height ), {lineWidth: 1, stroke: new LinearGradient( 0, 0, 0, height ).addColorStop( 0, 'black' ).addColorStop( 0.5, 'white' ).addColorStop( 1, 'black' ) } );
    return new Node( {children: [leftBar, rightBar], visible: false} );
  };

  //TODO this is copied from NavigationBar
  var createHighlightListener = function( node ) {
    return {//Highlight a button when mousing over it
      over: function( event ) { if ( event.pointer.isMouse ) {node.visible = true;} },
      out: function( event ) { if ( event.pointer.isMouse ) {node.visible = false;} }
    };
  };

  //TODO don't pass in navigationBar, position based on this button
  function PhetButton( sim, options ) {

    var phetButton = this;
    Node.call( this, {renderer: 'svg', cursor: 'pointer'} );

    options = _.extend( {phetLogo: phetLogo, phetLogoScale: 0.28, optionsButtonVerticalMargin: 1.5}, options );

    var phetLabel = new Image( options.phetLogo, {scale: options.phetLogoScale} );

    //Workaround for the SVG not showing up properly in firefox.  SVG Renderer still giving odd bounds on FireFox, so use canvas there
    if ( platform.firefox ) {phetLabel.renderer = 'canvas';}

    var optionsButton = new FontAwesomeNode( 'reorder', {fill: '#fff', scale: 0.6, left: phetLabel.width + 10, bottom: phetLabel.bottom - options.optionsButtonVerticalMargin} );

    this.addChild( phetLabel );
    this.addChild( optionsButton );

    var optionsHighlight = createHighlight( this.width + 6, this.height - 2 );
    optionsHighlight.bottom = this.bottom + 3;
    optionsHighlight.x = -3;
    this.addChild( optionsHighlight );

    //When the phet button is pressed, show the phet menu
    var phetButtonPressed = function() {
      var phetMenu = new PhetMenu( sim );
      phetMenu.right = phetButton.globalToParentPoint( new Vector2( phetButton.globalBounds.maxX, 0 ) ).x;
      phetMenu.bottom = phetButton.centerY;
      var rectangle = new Plane( {fill: 'black', opacity: 0.3} );
      var detach = function() {
        rectangle.detach();
        phetMenu.detach();
        phetMenu.removeInputListener( popupMenuListener );
        rectangle.removeInputListener( rectangleListener );
      };
      var popupMenuListener = new ButtonListener( {fire: detach} );
      var rectangleListener = {down: detach};

      phetMenu.addInputListener( popupMenuListener );
      rectangle.addInputListener( rectangleListener );

      phetButton.parents[0].addChild( rectangle );
      phetButton.parents[0].addChild( phetMenu );
    };

    this.addPeer( '<input type="button" aria-label="PhET Menu">', {click: phetButtonPressed, tabIndex: 101} );
    this.addInputListener( new ButtonListener( {fire: phetButtonPressed} ) );
    this.addInputListener( createHighlightListener( optionsHighlight ) );

    // eliminate interactivity gap between label and button
    this.mouseArea = this.touchArea = Shape.bounds( this.bounds );

    this.mutate( options );
  }

  return inherit( Node, PhetButton );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Static utilities for creating and showing highlights on mouseover
 *
 * @author Sam Reid
 */
define('JOIST/Highlight',['require','SCENERY/nodes/Node','SCENERY/nodes/Path','KITE/Shape','SCENERY/util/LinearGradient'], function( require ) {
  

  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );

  var createHighlight = function( width, height ) {
    var leftBar = new Path( Shape.lineSegment( 0, 0, 0, height ), { lineWidth: 1, stroke: new LinearGradient( 0, 0, 0, height ).addColorStop( 0, 'black' ).addColorStop( 0.5, 'white' ).addColorStop( 1, 'black' ) } );
    var rightBar = new Path( Shape.lineSegment( width, 0, width, height ), {lineWidth: 1, stroke: new LinearGradient( 0, 0, 0, height ).addColorStop( 0, 'black' ).addColorStop( 0.5, 'white' ).addColorStop( 1, 'black' ) } );
    return new Node( {children: [leftBar, rightBar], visible: false} );
  };

  var createHighlightListener = function( node ) {
    return {//Highlight a button when mousing over it
      over: function( event ) { if ( event.pointer.isMouse ) {node.visible = true;} },
      out: function( event ) { if ( event.pointer.isMouse ) {node.visible = false;} }
    };
  };

  return {createHighlight: createHighlight, createHighlightListener: createHighlightListener};
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * The navigation bar at the bottom of the screen.
 * For a single-screen sim, it shows the name of the sim at the left and the PhET Logo and options menu at the right.
 * For a multi-screen sim, it shows icons for all of the other screens, with the screen name at the left and the PhET Logo and options menu at the right.
 *
 * @author Sam Reid
 */
define('JOIST/NavigationBar',['require','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY/nodes/HBox','SCENERY/nodes/VBox','SCENERY/nodes/Text','SUN/FontAwesomeNode','SUN/Panel','SCENERY_PHET/HomeButton','SCENERY/nodes/Rectangle','PHET_CORE/inherit','JOIST/PhetMenu','SCENERY_PHET/PhetFont','KITE/Shape','SCENERY/util/LinearGradient','JOIST/PhetButton','SCENERY/input/ButtonListener','JOIST/Highlight'], function( require ) {
  

  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var VBox = require( 'SCENERY/nodes/VBox' );
  var Text = require( 'SCENERY/nodes/Text' );
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var Panel = require( 'SUN/Panel' );
  var HomeButton = require( 'SCENERY_PHET/HomeButton' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var inherit = require( 'PHET_CORE/inherit' );
  var PhetMenu = require( 'JOIST/PhetMenu' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Shape = require( 'KITE/Shape' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var PhetButton = require( 'JOIST/PhetButton' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Highlight = require( 'JOIST/Highlight' );

  /**
   * Create a nav bar.  Layout assumes all of the screen widths are the same.
   * @param {Sim} sim
   * @param {Array<Screen>} screens
   * @param {PropertySet} model see joist.Sim
   * @constructor
   */
  function NavigationBar( sim, screens, model ) {
    this.screens = screens;

    this.navBarHeight = 40;
    this.navBarScale = 1;
    this.navBarWidth = 768;

    Node.call( this, {renderer: 'svg'} );
    this.background = new Rectangle( 0, 0, 0, 0, {fill: 'black'} );
    this.addChild( this.background );

    this.hbox = new PhetButton( sim );
    this.addChild( this.hbox );

    this.titleLabel = new Text( sim.name, {font: new PhetFont( 18 ), fill: 'white'} );

    //Create the nodes to be used for the screen icons
    var index = 0;

    var selectedFont = new PhetFont( { size: 10, weight: 'bold'} );
    var normalFont = new PhetFont( { size: 10 } );

    this.addChild( this.titleLabel );

    if ( screens.length > 1 ) {

      var iconAndTextArray = _.map( screens, function( screen ) {
        var icon = new Node( {children: [screen.icon], scale: 25 / screen.icon.height} );
        var text = new Text( screen.name, { fill: 'white', visible: true} );

        //Put a panel around it to extend it horizontally so there is some distance from the highlight region to the text and some distance between adjacent texts.
        var textPanel = new Panel( text, {
          fill: null,
          stroke: null,
          lineWidth: 1, // width of the background border
          xMargin: 4,
          yMargin: 0,
          cornerRadius: 0, // radius of the rounded corners on the background
          resize: false // dynamically resize when content bounds change?
        } );
        var iconAndText = new VBox( {children: [icon, textPanel]} );
        iconAndText.icon = icon;
        iconAndText.text = text;
        iconAndText.textPanel = textPanel;
        iconAndText.index = index++;
        iconAndText.screen = screen;

        //On initialization and when the screen changes, update the size of the icons and the layout of the icons and text
        model.screenIndexProperty.link( function( screenIndex ) {
          var selected = iconAndText.index === screenIndex;
          iconAndText.text.fill = selected ? 'yellow' : 'white';
          iconAndText.text.font = selected ? selectedFont : normalFont;
          iconAndText.opacity = selected ? 1.0 : 0.5;
        } );

        return iconAndText;
      } );

      //Make all of the icons the same size so they will have equal hit areas and equal spacing
      var maxWidth = _.max( iconAndTextArray,function( iconAndText ) {return iconAndText.width;} ).width;
      var maxHeight = _.max( iconAndTextArray,function( iconAndText ) {return iconAndText.height;} ).height;

      this.buttonArray = iconAndTextArray.map( function( iconAndText ) {
        //Background area for layout and hit region
        var rectangle = new Rectangle( 0, 0, maxWidth, maxHeight );
        var highlight = Highlight.createHighlight( maxWidth, maxHeight );
        iconAndText.centerX = maxWidth / 2;
        iconAndText.top = 0;
        var button = new Node( {children: [ rectangle, highlight, iconAndText]} );

        //In the nav bar, don't show highlight or cursor pointer when over a screen icon that is already selected
        model.screenIndexProperty.link( function( screenIndex ) { button.cursor = screenIndex === iconAndText.index ? 'default' : 'pointer'; } );

        var pressListener = function() {
          model.screenIndex = iconAndText.index;
          model.showHomeScreen = false;
        };
        button.addInputListener( new ButtonListener( {fire: pressListener} ) );

        //TODO: Move the over/out listener into ButtonListener when it is working better. Currently 'out' is not being fired
        button.addInputListener( {
          //Highlight a button when mousing over it
          over: function( event ) {
            if ( event.pointer.isMouse ) {
              highlight.visible = true;
            }
          },
          out: function( event ) {
            if ( event.pointer.isMouse ) {
              highlight.visible = false;
            }
          }
        } );

        button.addPeer( '<input type="button" aria-label="Switch to the ' + iconAndText.screen.name + ' screen">', {click: pressListener, tabIndex: 99} );
        return  button;
      } );

      //TODO: This spacing is not always necessary, it depends on the relative width
      //TODO:   of the icon vs the text, if all of the buttons have the same dimensions.
      //TODO:   Currently this solves the simple case where all of the text is shorter than all of the icons (like in Build an Atom)
      //TODO: A better strategy may be to use a linear function to space them based on how far it is from the criterion maxTextWidth<=maxIconWidth+2
      //See Joist #28 https://github.com/phetsims/joist/issues/28
      var maxIconWidth = _.max( iconAndTextArray,function( i ) {return i.icon.width;} ).icon.width;
      var maxTextWidth = _.max( iconAndTextArray,function( i ) {return i.text.width;} ).text.width;

      this.buttonHBox = new HBox( {spacing: maxTextWidth <= maxIconWidth + 2 ? 20 : 0, children: this.buttonArray} );
      this.addChild( this.buttonHBox );

      //add the home icon
      this.homeIcon = new HomeButton();
      var homeHighlight = Highlight.createHighlight( this.homeIcon.width + 10, this.homeIcon.height + 5 );
      homeHighlight.bottom = this.homeIcon.bottom + 3;
      homeHighlight.x = -5;
      this.homeIcon.addChild( homeHighlight );

      //Hide the highlight on the home icon if the home icon is pressed
      model.showHomeScreenProperty.link( function( showHomeScreen ) { if ( showHomeScreen ) { homeHighlight.visible = false; } } );
      this.homeIcon.addInputListener( { down: function() { model.showHomeScreen = true; }} );
      this.homeIcon.addInputListener( Highlight.createHighlightListener( homeHighlight ) );
      this.homeIcon.addPeer( '<input type="button" aria-label="Home Screen">', {click: function() {model.showHomeScreen = true;}, tabIndex: 100} );
      this.addChild( this.homeIcon );
    }
  }

  return inherit( Node, NavigationBar, {
    relayout: function() {
      var navigationBar = this;
      navigationBar.background.rectHeight = this.navBarHeight;
      navigationBar.background.rectWidth = this.navBarWidth;
      var screenIndex = navigationBar.screenIndex;

      if ( this.buttonHBox ) {
        this.buttonHBox.setScaleMagnitude( navigationBar.navBarScale );
      }

      this.titleLabel.setScaleMagnitude( this.navBarScale );
      this.titleLabel.centerY = this.navBarHeight / 2;
      this.titleLabel.left = 10;

      //Lay out the components from left to right
      if ( this.screens.length !== 1 ) {

        //put the center right in the middle
        this.buttonHBox.centerX = this.navBarWidth / 2;
        this.buttonHBox.top = 2;

        navigationBar.homeIcon.setScaleMagnitude( this.navBarScale );
        navigationBar.homeIcon.top = 2;
        navigationBar.homeIcon.left = navigationBar.buttonHBox.right + 15;
      }
      this.hbox.setScaleMagnitude( this.navBarScale );
      this.hbox.right = this.navBarWidth - 5;
      this.hbox.centerY = this.navBarHeight / 2;
    },
    layout: function( scale, width, height, windowHeight ) {
      this.navBarScale = scale;
      this.navBarWidth = width;
      this.navBarHeight = height;
      this.relayout();
    }} );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Decorative frame around the selected node
 *
 * @author Sam Reid
 */
define('JOIST/Frame',['require','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Rectangle','SCENERY/util/LinearGradient'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );

  /**
   * @param {Node} content
   * @param {object} options
   * @constructor
   */
  function Frame( content, options ) {

    var thisNode = this;

    // default options
    options = _.extend( {
      xMargin1: 6,
      yMargin1: 6,
      cornerRadius: 0 // radius of the rounded corners on the background
    }, options );

    Node.call( thisNode );

    var width1 = content.width + 2 * options.xMargin1;
    var height1 = content.height + 2 * options.yMargin1;

    this.gradient = new LinearGradient( 0, 0, width1, 0 ).addColorStop( 0, '#fbff41' ).addColorStop( 118 / 800.0, '#fef98b' ).addColorStop( 372 / 800.0, '#feff40' ).addColorStop( 616 / 800, '#fffccd' ).addColorStop( 1, '#fbff41' );
    this.rectangle = new Rectangle( 0, 0, width1, height1, options.cornerRadius, options.cornerRadius, {stroke: this.gradient, lineWidth: 3, x: content.x - options.xMargin1, y: content.y - options.yMargin1} );
    this.addChild( this.rectangle );

    // Apply options after the layout is done, so that options that use the bounds will work properly.
    this.mutate( options );
    this.width1 = width1;
    this.height1 = height1;
  }

  inherit( Node, Frame, {
    setHighlighted: function( highlighted ) {
      this.rectangle.lineWidth = highlighted ? 4.5 : 3;

      //Make the frame larger when highlighted, but only so that it expands out
      if ( highlighted ) {
        this.rectangle.setRect( -1.5 / 2, -1.5 / 2, this.width1 + 1.5, this.height1 + 1.5 );
      }
      else {
        this.rectangle.setRect( 0, 0, this.width1, this.height1 );
      }
    }
  } );

  return Frame;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Shows a button that changes the simulation to full screen.  This feature is not available on iPad.
 *
 * @author Sam Reid
 */
define('JOIST/FullScreenButton',['require','SCENERY/nodes/Node','SCENERY/nodes/Path','KITE/Shape','SCENERY/util/LinearGradient','SUN/FontAwesomeNode','PHET_CORE/inherit','SCENERY/input/ButtonListener','SCENERY/input/Input'], function( require ) {
  

  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Input = require( 'SCENERY/input/Input' );

  function requestFullScreen( element ) {

    // Supports most browsers and their versions.
    var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;

    if ( requestMethod ) { // Native full screen.
      requestMethod.call( element );
    }
    else if ( typeof window.ActiveXObject !== "undefined" ) { // Older IE.
      var wscript = new ActiveXObject( "WScript.Shell" );
      if ( wscript !== null ) {
        wscript.SendKeys( "{F11}" );
      }
    }
  }

  var fullScreener = function() {
    requestFullScreen( document.body );
  };

  function FullScreenButton( options ) {
    Node.call( this, {cursor: 'pointer'} );
    this.addChild( new FontAwesomeNode( 'fullscreen', {fill: '#fff', scale: 0.8} ) );
    this.mutate( _.extend( {cursor: 'pointer'}, options ) );//TODO: pointer not going through here

    this.addInputListener( {down: function() {
      $( 'body' ).one( 'mouseup', fullScreener );
    }} );

    this.mouseArea = this.bounds;
    this.touchArea = this.bounds;
  }

  return inherit( Node, FullScreenButton );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Shows the home screen for a multi-screen simulation, which lets the user see all of the screens and select one.
 *
 * @author Sam Reid
 */
define('JOIST/HomeScreen',['require','JOIST/PhetButton','SCENERY/nodes/Node','SCENERY/nodes/HBox','SCENERY/nodes/VBox','SCENERY/nodes/Text','KITE/Shape','PHET_CORE/inherit','PHET_CORE/platform','JOIST/ScreenView','JOIST/Frame','SCENERY/nodes/Rectangle','AXON/Property','SCENERY/input/ButtonListener','JOIST/FullScreenButton','image!JOIST/../images/phet-logo-short.svg','SCENERY_PHET/PhetFont'], function( require ) {
  

  var PhetButton = require( 'JOIST/PhetButton' );
  var Node = require( 'SCENERY/nodes/Node' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var VBox = require( 'SCENERY/nodes/VBox' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Shape = require( 'KITE/Shape' );
  var inherit = require( 'PHET_CORE/inherit' );
  var platform = require( 'PHET_CORE/platform' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var Frame = require( 'JOIST/Frame' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Property = require( 'AXON/Property' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var FullScreenButton = require( 'JOIST/FullScreenButton' );
  var phetLogo = require( 'image!JOIST/../images/phet-logo-short.svg' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );

  var HEIGHT = 70;

  function HomeScreen( sim ) {
    var homeScreen = this;

    //Rendering in SVG seems to solve the problem that the home screen consumes 100% disk and crashes, see https://github.com/phetsims/joist/issues/17
    //Also makes it more responsive (and crisper on retina displays)
    ScreenView.call( this, {renderer: 'svg'} );

    this.backgroundColor = 'black';

    //iPad doesn't support Century Gothic, so fall back to Futura, see http://wordpress.org/support/topic/font-not-working-on-ipad-browser
    var title = new Text( sim.name, {
      font: new PhetFont( {size: 52, family: 'Century Gothic, Futura' } ),
      fill: 'white',
      y: 110,
      centerX: this.layoutBounds.width / 2} );
    this.addChild( title );

    //Keep track of which screen is highlighted so the same screen can remain highlighted even if nodes are replaced (say when one grows larger or smaller)
    var highlightedIndex = new Property( -1 );

    var screenChildren = _.map( sim.screens, function( screen ) {
      var index = sim.screens.indexOf( screen );
      var largeIcon = new Node( {children: [screen.icon], scale: HEIGHT / screen.icon.height * 2} );
      var frame = new Frame( largeIcon );

      highlightedIndex.link( function( highlightedIndex ) { frame.setHighlighted( highlightedIndex === index ); } );

      var largeIconWithFrame = new Node( {children: [ frame, largeIcon]} );
      var large = new VBox( {cursor: 'pointer', children: [
        largeIconWithFrame,
        new Text( screen.name, { font: new PhetFont( 42 ), fill: 'yellow'} )
      ]} );

      //TODO: Switch to buttonListener, but make sure you test it because on 7/17/2013 there is a problem where ButtonListener won't fire if a node has appeared under the pointer
      large.addInputListener( {
        down: function() {
          sim.simModel.showHomeScreen = false;
          highlightedIndex.value = -1;
        }
      } );

      var small = new VBox( {spacing: 3, cursor: 'pointer', children: [
        new Node( {opacity: 0.5, children: [screen.icon], scale: sim.screens.length === 4 ? HEIGHT / screen.icon.height :
                                                                 sim.screens.length === 3 ? 1.25 * HEIGHT / screen.icon.height :
                                                                 sim.screens.length === 2 ? 1.75 * HEIGHT / screen.icon.height :
                                                                 HEIGHT / screen.icon.height} ),
        new Text( screen.name, { font: new PhetFont( 18 ), fill: 'gray'} )
      ]} );
      small.mouseArea = small.touchArea = Shape.bounds( small.bounds ); //cover the gap in the vbox
      small.addInputListener( {
        down: function() { sim.simModel.screenIndex = index; },

        //On the home screen if you touch an inactive screen thumbnail, it grows.  If then without lifting your finger you swipe over
        // to the next thumbnail, that one would grow.
        over: function( event ) {
          if ( !event.pointer.isMouse ) {
            sim.simModel.screenIndex = index;
          }
        }
      } );

      var highlightListener = {
        over: function( event ) {
          if ( event.pointer.isMouse ) {
            highlightedIndex.value = index;

            //TODO: use named children instead of child indices?
            small.children[0].opacity = 1;
            small.children[1].fill = 'white';
          }
        },
        out: function( event ) {
          if ( event.pointer.isMouse ) {
            highlightedIndex.value = -1;
            small.children[0].opacity = 0.5;
            small.children[1].fill = 'gray';
          }
        }
      };
      small.addInputListener( highlightListener );

      large.addInputListener( highlightListener );
      large.mouseArea = large.touchArea = Shape.bounds( large.bounds ); //cover the gap in the vbox

      //TODO: Add accessibility peers
      //      screenChild.addPeer( '<input type="button" aria-label="' + screenChild.screen.name + '">', {click: function() {
//        var screen = screenChild.screen;
//        if ( sim.simModel.screenIndex === screen.index ) {
//          sim.simModel.showHomeScreen = false;
//        }
//        else {
//          sim.simModel.screenIndex = screen.index;
//        }
//      }} );
//    } );

      return {screen: screen, small: small, large: large, index: index};
    } );

    var center = new Node( {y: 170} );
    homeScreen.addChild( center );
    sim.simModel.screenIndexProperty.link( function( screenIndex ) {

      //Space the icons out more if there are fewer, so they will be spaced nicely
      //Cannot have only 1 screen because for 1-screen sims there is no home screen.
      var spacing = sim.screens.length === 2 ? 60 :
                    sim.screens.length === 3 ? 60 :
                    33;

      var icons = _.map( screenChildren, function( screenChild ) {return screenChild.index === screenIndex ? screenChild.large : screenChild.small;} );
      center.children = [new HBox( {spacing: spacing, children: icons, align: 'top'} )];
      center.centerX = homeScreen.layoutBounds.width / 2;
    } );

    //Only show the full screen button on supported platforms
    var showFullScreenButton = !platform.android && !platform.mobileSafari && !platform.ie; // might work on IE11 in the future
    if ( showFullScreenButton && false ) {
      var fullScreenButton = new FullScreenButton();
      var phetButton = new PhetButton( sim );
      this.addChild( new HBox( {spacing: 10, children: [fullScreenButton, phetButton], right: this.layoutBounds.maxX - 5, bottom: this.layoutBounds.maxY - 5} ) );
    }
    else {
      this.addChild( new PhetButton( sim, {phetLogo: phetLogo, phetLogoScale: 0.4, optionsButtonVerticalMargin: 6} ).mutate( {right: this.layoutBounds.maxX - 5, bottom: this.layoutBounds.maxY - 5} ) );
    }
  }

  return inherit( ScreenView, HomeScreen );
} );

// Copyright 2002-2013, University of Colorado Boulder

// Returns version identifier for the simulation.
define('version',[], function() {
  
  return function() { return '1.1.0-dev.1'; };
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * A DerivedProperty is computed based on other properties.
 *
 * TODO: Inherit from ObservableProperty?
 * @author Sam Reid
 */

define('AXON/DerivedProperty',['require','AXON/Property','AXON/axon'], function( require ) {
  

  var Property = require( 'AXON/Property' );
  var axon = require( 'AXON/axon' );

  /**
   * @param {Array<Property>} dependencies
   * @param {Function} derivation function that expects args in the same order as dependencies
   * @constructor
   */
  axon.DerivedProperty = function DerivedProperty( dependencies, derivation ) {

    this.observers = [];
    this.dependencies = dependencies;

    var derivedProperty = this;

    //When any of the dependencies change, see if the value has changed
    //If the value has changed, send out notification.
    //TODO: Move to prototype?
    function update() {

      //TODO: Could just re-evaluate the changed property instead of recomputing all of them, right?
      var args = dependencies.map( function( property ) { return property.get(); } );
      var newValue = derivation.apply( null, args );

      //Send out notifications if the value has truly changed
      if ( newValue !== derivedProperty._value ) {
        var oldValue = derivedProperty._value;
        derivedProperty._value = newValue; //Store the value so it can be compared against next time, and for the value getter
        derivedProperty.observers.forEach( function( observer ) { observer( newValue, oldValue ); } );
      }
    }

    this.update = update;

    dependencies.forEach( function( property ) {
      property.lazyLink( update );
    } );

    //TODO: Should we add an option to defer callbacks (like lazyLink?)

    //Call the derivation function with the initial value(s)
    update();
  };

  //TODO: _value could be made private if we moved these functions to the constructor, but I don't think it is necessary
  axon.DerivedProperty.prototype = {

    /**
     * Get the current value of this DerivedProperty.
     * @returns {*}
     */
    get value() {
      return this._value;
    },

    get: function() {
      return this._value;
    },

    /**
     * Add an observer to this DerivedProperty
     * @param observer
     */
    link: function( observer ) {
      this.observers.push( observer );
      observer( this._value );
    },

    /**
     * Link to a target object's attribute, see Property.linkAttribute
     * @param object
     * @param attributeName
     * @returns {Function}
     */
    linkAttribute: function( object, attributeName ) {
      var handle = function( value ) {object[attributeName] = value;};
      this.link( handle );
      return handle;
    },

    unlink: function( observer ) {
      var index = this.observers.indexOf( observer );
      if ( index !== -1 ) {
        this.observers.splice( index, index + 1 );
      }
    },

    /**
     * @see Property.lazyLink
     * @param observer
     */
    lazyLink: function( observer ) {
      this.observers.push( observer );
    },

    /**
     * Detaches this derived property from its dependencies.
     */
    detach: function() {
      var derivedProperty = this;
      this.dependencies.forEach( function( property ) {
        property.unlink( derivedProperty.update );
      } );
    }
  };

  return axon.DerivedProperty;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * PropertySet facilitates creation and use of multiple named Property instances.  There are still several API design issues in question, but this
 * class is ready for use.
 *
 * A PropertySet is a set of Property instances that provides support for:
 * -Easily creating several properties using an object literal (hash)
 * -Resetting them as a group
 * -Set multiple values at once, using propertySet.set({x:100,y:200,name:'alice'});
 * -Support for derived properties, which appear with the same interface as basic properties
 * -Convenient toString that prints e.g., PropertySet{name:'larry',age:101,kids:['alice','bob']}
 * -Wiring up to listen to multiple properties simultaneously
 * -Add properties after the PropertySet is created?  Don't forget to add to the key list as well.
 * -Remove properties that were added using addProperty or the constructor
 * -TODO: Make it easy to mix-in with model classes?  Subclassing PropertySet already works fairly well, so this may good enough already.
 * -TODO: Type checking, so that a boolean input will be automatically generated as BooleanProperty, etc.
 * -TODO: Should this be called Model or perhaps something even better?
 * -TODO: addProperty(DerivedProperty/Property)???  Perhaps overload it?  Let's wait on that until we need it.
 *
 * Sample usage:
 * var p = new PropertySet( {name: 'larry', age: 100, kids: ['alice', 'bob']} );
 * p.nameProperty.link( function( n ) {console.log( 'hello ' + n );} );
 * p.name = 'jensen';
 * p.age = 101;//Happy Birthday!
 * console.log( p );
 * p.reset();
 * console.log( p );
 * p.set({name:'clark',age:102,kids:['alice','bob','charlie']});
 * p.values = {name:'clark',age:102,kids:['alice','bob','charlie']}; //TODO: should we support this es5 way of doing it?  I kind of like it better than set
 *
 * How would this be done without PropertySet (for comparison)?
 * //Normally would be created in a class but that is omitted here for brevity.
 * var p ={name: new Property('larry'), age: new Property('age'), kids: new Property(['alice','bob'])}
 * p.reset = function(){
 *   this.name.reset(); 
 *   this.age.reset();
 *   this.kids.reset();
 * }
 * p.name.set('clark');
 * p.age.set('102');
 * p.kids.set(['alice','bob','charlie']);
 *
 * @author Sam Reid
 */

define('AXON/PropertySet',['require','AXON/Property','AXON/DerivedProperty','AXON/axon'], function( require ) {
  

  var Property = require( 'AXON/Property' );
  var DerivedProperty = require( 'AXON/DerivedProperty' );
  var axon = require( 'AXON/axon' );

  /**
   * @class PropertySet
   * @constructor
   * @param values an object hash with the initial values for the properties
   */
  axon.PropertySet = function PropertySet( values ) {
    var propertySet = this;

    //Keep track of the keys so we know which to reset
    this.keys = [];

    Object.getOwnPropertyNames( values ).forEach( function( value ) {
      propertySet.addProperty( value, values[value] );
    } );

    this.eventListeners = {};
  };

  axon.PropertySet.prototype = {

    /**
     * Adds a new property to this PropertySet
     *
     * @param {String} name
     * @param value
     */
    addProperty: function( name, value ) {
      this[name + 'Property'] = new Property( value );
      this.addGetterAndSetter( name );
      this.keys.push( name );
    },

    /**
     * Remove any property (whether a derived property or not) that was added to this PropertySet
     * @param name
     */
    removeProperty: function( name ) {

      //Remove from the keys (only for non-derived properties)
      var index = this.keys.indexOf( name );
      if ( index !== -1 ) {
        this.keys.splice( index, index + 1 );
      }

      //Unregister the Property instance from the PropertySet
      delete this[name + 'Property'];

      //Unregister the getter/setter, if they exist
      delete this[name];
    },

    //Taken from https://gist.github.com/dandean/1292057, same as in github/Atlas
    addGetterAndSetter: function( name ) {
      var propertyName = name + 'Property';

      //TODO: Store the this[propertyName] in a closure for performance?  Memory/performance tradeoff, and problems if the property instance ever changes (unlikely)
      //TODO: If a subclass ever substitutes a property like this: person.ageProperty = new Property(person.age), then it would break the getter/setter
      Object.defineProperty( this, name, {

        // Getter proxies to Model#get()...
        get: function() { return this[propertyName].get();},

        // Setter proxies to Model#set(attributes)
        set: function( value ) { this[propertyName].set( value );},

        // Make it configurable and enumerable so it's easy to override...
        configurable: true,
        enumerable: true
      } );
    },

    addGetter: function( name ) {

      //TODO: Store the this[propertyName] for performance?
      var propertyName = name + 'Property';
      Object.defineProperty( this, name, {

        get: function() { return this[propertyName].get();},

        // Make it configurable and enumerable so it's easy to override...
        configurable: true,
        enumerable: true
      } );
    },

    //Resets all of the properties associated with this PropertySet 
    reset: function() {
      var propertySet = this;
      this.keys.forEach( function( key ) {
        propertySet[key + 'Property'].reset();
      } );
    },

    /**
     * Creates a DerivedProperty from the given dependency names and derivation.
     * @param dependencyNames {Array<String>}
     * @param derivation {Function}
     * @returns {DerivedProperty}
     */
    toDerivedProperty: function( dependencyNames, derivation ) {
      var propertySet = this;
      var dependencies = dependencyNames.map( function( dependency ) {
        return propertySet[dependency + 'Property'];
      } );
      return new DerivedProperty( dependencies, derivation );
    },

    addDerivedProperty: function( name, dependencyNames, derivation ) {
      this[name + 'Property'] = this.toDerivedProperty( dependencyNames, derivation );
      this.addGetter( name );
    },

    /**
     * Set all of the values specified in the object hash
     * Allows you to use this form:
     * puller.set( {x: knot.x, y: knot.y, knot: knot} );
     *
     * instead of this:
     * puller.x.value = knot.x;
     * puller.y.value = knot.y;
     * puller.knot.value = knot;
     *
     * Throws an error if you try to set a value for which there is no property.
     */
    set: function( values ) {
      var propertySet = this;
      Object.getOwnPropertyNames( values ).forEach( function( val ) {
        if ( typeof(propertySet[val + 'Property'] === 'Property') ) {
          propertySet[val + 'Property'].set( values[val] );
        }
        else {
          throw new Error( 'property not found: ' + val );
        }
      } );
    },

    /**
     * Add a listener to zero or more properties in this PropertySet, useful when you have an update function
     * that relies on several properties.  Similar to DerivedProperty.
     * TODO: Should this be named link because it won't clash with any other methods on this class?
     *  Discussion result: Let's use 'multilink' for now, and in the future we may change it to link.
     * @param dependencyNames {Array<String>} the list of dependencies to use
     * @param listener {Function} the listener to call back, with signature matching the dependency names
     */
    multilink: function( dependencyNames, listener ) {
      return this.toDerivedProperty( dependencyNames, listener );
    },

    /**
     * Removes the multilinked listener from this PropertySet.
     * Same as calling detach() on the handle (which happens to be a DerivedProperty instance)
     * @param derivedProperty
     */
    unmultilink: function( derivedProperty ) {
      derivedProperty.detach();
    },

    toString: function() {
      var text = 'PropertySet{';
      var propertySet = this;
      for ( var i = 0; i < this.keys.length; i++ ) {
        var key = this.keys[i];
        text = text + key + ':' + propertySet[key].toString();
        if ( i < this.keys.length - 1 ) {
          text = text + ',';
        }
      }
      return text + '}';
    },

    /////////////////////////////////////////////
    // Below this point are the functions for event handling, basically orthogonal to property value change notifications

    /**
     * Register a listener when the specified eventName is triggered.
     * @param eventName {String} the name for the event channel
     * @param callback
     */
    on: function( eventName, callback ) {
      this.eventListeners[eventName] = this.eventListeners[eventName] || [];
      this.eventListeners[eventName].push( callback );
    },

    /**
     * Adds a function which will only be called back once, after which it is removed as a listener.
     * If you need to remove a function added with 'once' you will have to remove its handle, which is returned by the function.
     * @param eventName {String} the name for the event channel
     * @param callback function to be called back once (if at all)
     */
    once: function( eventName, callback ) {
      var propertySet = this;
      var wrappedCallback = function() {
        propertySet.off( eventName, wrappedCallback );

        //If no arguments being passed through, call back without processing arguments, for possible speed
        if ( arguments.length === 0 ) {
          callback();
        }
        else {

          //General case of passing events through to the wrapped callback function
          callback.apply( this, Array.prototype.slice.call( arguments, 0 ) );
        }
      };
      this.on( eventName, wrappedCallback );

      //Return the handle in case it needs to be removed.
      return wrappedCallback;
    },

    /**
     * Remove a listener from the specified event type.  Does nothing if the listener did not exist
     * @param eventName {String} the name for the event channel
     * @param callback
     */
    off: function( eventName, callback ) {
      if ( this.eventListeners[eventName] ) {
        var index = this.eventListeners[eventName].indexOf( callback );
        if ( index !== -1 ) {
          this.eventListeners[eventName].splice( index, index + 1 );
        }
      }
    },

    /**
     * Trigger an event with the specified name and arguments.
     * @param eventName {String} the name for the event channel
     * @param args... optional arguments to pass to the listeners
     */
    trigger: function( eventName ) {
      if ( this.eventListeners[eventName] ) {
        var listenersCopy = this.eventListeners[eventName].slice(); // make a copy, in case callback removes listener
        for ( var i = 0; i < listenersCopy.length; i++ ) {
          var listener = listenersCopy[i];

          //Simple case of no arguments, call it separately for improved performance in case it is faster (untested)
          if ( arguments.length === 1 ) {
            listener( arguments );
          }
          else {
            var suffix = Array.prototype.slice.call( arguments, 1 );
            listener.apply( this, suffix );
          }
        }
      }
    }
  };

  return axon.PropertySet;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * This model implementation tracks the location of the pointer(s), if any, so they can be visualized during playback.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define('JOIST/share/Pointer',['require'], function( require ) {
  

  function Pointer() {}

  return Pointer;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * This model implementation tracks the location of the pointer(s), if any, so they can be visualized during playback.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define('JOIST/share/Pointers',['require','JOIST/share/Pointer'], function( require ) {
  

  var Pointer = require( 'JOIST/share/Pointer' );

  //TODO: make it an observable array 
  return [];
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * This model implementation tracks the location of the pointer(s), if any, so they can be visualized during playback.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define('JOIST/share/LogPointers',['require','JOIST/share/Pointer','JOIST/share/Pointers'], function( require ) {
  

  var Pointer = require( 'JOIST/share/Pointer' );
  var Pointers = require( 'JOIST/share/Pointers' );

  function LogPointers() {

    //Set up data logging and visualization
    this.pointers = new Pointers();
  }

  LogPointers.prototype = {
    startLogging: function() {
      var logPointers = this;
      window.addEventListener( 'mousemove', function( e ) {
        var location = {x: e.clientX, y: e.clientY};
        if ( logPointers.pointers.length === 0 ) {
          logPointers.pointers.add( new Pointer( location ) );
        }
        else {
          logPointers.pointers.at( 0 ).set( location );
        }
      }, false );
      window.addEventListener( 'touchmove', function( e ) {
        var touches = e.touches;
        for ( var i = 0; i < touches.length; i++ ) {
          var touch = touches[i];
          var location = {x: touch.pageX, y: touch.pageY};
          if ( i >= logPointers.pointers.length ) {
            logPointers.pointers.add( new Pointer( location ) );
          }
          else {
            logPointers.pointers.at( i ).set( location );
          }
        }
        while ( logPointers.pointers.length > touches.length ) {
          logPointers.pointers.pop();
        }
      }, false );

      //TODO: add touchend support
    },
    showPointers: function() {
      var logPointers = this;
      this.pointers.on( 'add', function( model, collection, options ) {
        var $img = $( '<img id="cursor">' ); //Equivalent: $(document.createElement('img'))
        $img.attr( 'src', 'http://dc440.4shared.com/img/mFJBl0A0/s7/mouse-cursor-icon.png' ); //TODO: our own copy of the image
        $img.css( {zIndex: 9999, position: 'absolute', width: 12, height: 20, 'pointer-events': 'none'} );
        $img.appendTo( 'body' );
        model.on( 'change:x change:y', function() {
          $img.css( {left: model.x, top: model.y} );
        } );

        logPointers.pointers.on( 'remove', function( m, c, o ) {
          if ( m === model ) {
            $img.detach();
            //TODO: clean up listeners?
          }
        } );
      } );
    }
  };

  return LogPointers;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Main class that represents one simulation.
 * Provides default initialization, such as polyfills as well.
 * If the simulation has only one screen, then there is no homescreen, home icon or screen icon in the navigation bar.
 *
 * @author Sam Reid
 */
define('JOIST/Sim',['require','SCENERY/util/Util','JOIST/NavigationBar','JOIST/HomeScreen','SCENERY/Scene','SCENERY/nodes/Node','SCENERY/nodes/Text','DOT/Vector2','DOT/Bounds2','version','AXON/PropertySet','AXON/Property','SCENERY_PHET/PhetFont','AXON/log','JOIST/share/LogPointers'], function( require ) {
  

  var Util = require( 'SCENERY/util/Util' );
  var NavigationBar = require( 'JOIST/NavigationBar' );
  var HomeScreen = require( 'JOIST/HomeScreen' );
  var Scene = require( 'SCENERY/Scene' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Vector2 = require( 'DOT/Vector2' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var version = require( 'version' );
  var PropertySet = require( 'AXON/PropertySet' );
  var Property = require( 'AXON/Property' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );

  //For Data logging and visualization
  var log = require( 'AXON/log' );
  var LogPointers = require( 'JOIST/share/LogPointers' );

  /**
   * @param {String} name
   * @param {Array<Screen>} screens
   * @param options optional parameters for starting screen and home values, so that developers can easily specify the startup scenario for quick development
   * @constructor
   */
  function Sim( name, screens, options ) {

    options = _.extend( { showHomeScreen: true, screenIndex: 0, standalone: false, credits: '', thanks: '' }, options );
    this.options = options; // store this for access from prototype functions, assumes that it won't be changed later

    this.destroyed = false;
    var sim = this;
    window.sim = sim;

    sim.name = name;
    sim.version = version();
    sim.credits = options.credits;
    sim.thanks = options.thanks;

    sim.frameCounter = 0; // number of animation frames that have occurred

    sim.inputEventLog = []; // used to store input events and requestAnimationFrame cycles
    sim.inputEventBounds = Bounds2.NOTHING;

    // state for mouse event fuzzing
    sim.fuzzMouseAverage = 10; // average number of mouse events to synthesize per frame
    sim.fuzzMouseIsDown = false;
    sim.fuzzMousePosition = new Vector2(); // start at 0,0
    sim.fuzzMouseLastMoved = false; // whether the last mouse event was a move (we skew probabilities based on this)

    //Set the HTML page title to the localized title
    //TODO: When a sim is embedded on a page, we shouldn't retitle the page
    $( 'title' ).html( name + ' ' + sim.version ); //TODO i18n of order

    //if nothing else specified, try to use the options for showHomeScreen & screenIndex from query parameters, to facilitate testing easily in different screens
    function stringToBoolean( string ) { return string === 'true' ? true : false; }

    // Query parameters override options.
    if ( window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'showHomeScreen' ) ) {
      options.showHomeScreen = stringToBoolean( window.phetcommon.getQueryParameter( 'showHomeScreen' ) );
    }
    if ( window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'screenIndex' ) ) {
      options.screenIndex = parseInt( window.phetcommon.getQueryParameter( 'screenIndex' ), 10 );
    }
    if ( window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'recordInputEventLog' ) ) {
      // enables recording of Scenery's input events, request animation frames, and dt's so the sim can be played back
      options.recordInputEventLog = true;
      options.inputEventLogName = window.phetcommon.getQueryParameter( 'recordInputEventLog' );
    }
    if ( window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'playbackInputEventLog' ) ) {
      // instead of loading like normal, download a previously-recorded event sequence and play it back (unique to the browser and window size)
      options.playbackInputEventLog = true;
      options.inputEventLogName = window.phetcommon.getQueryParameter( 'playbackInputEventLog' );
    }
    if ( window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'fuzzMouse' ) ) {
      // ignore any user input events, and instead fire mouse events randomly in an effort to cause an exception
      options.fuzzMouse = true;
      if ( window.phetcommon.getQueryParameter( 'fuzzMouse' ) !== 'undefined' ) {
        sim.fuzzMouseAverage = parseFloat( window.phetcommon.getQueryParameter( 'fuzzMouse' ) );
      }
    }
    if ( window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'fuzzTouches' ) ) {
      // ignore any user input events, and instead fire touch events randomly in an effort to cause an exception
      options.fuzzTouches = true;
    }

    //If specifying 'standalone' then filter the screens array so that it is just the selected screenIndex
    if ( window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'standalone' ) ) {
      options.standalone = true;
      screens = [screens[options.screenIndex]];
      options.screenIndex = 0;
    }

    //Default values are to show the home screen with the 1st screen selected
    var showHomeScreen = ( _.isUndefined( options.showHomeScreen ) ) ? true : options.showHomeScreen;

    //If there is only one screen, do not show the home screen
    if ( screens.length === 1 ) {
      showHomeScreen = false;
    }

    sim.screens = screens;

    //This model represents where the simulation is, whether it is on the home screen or a screen, and which screen it is on or is highlighted in the homescreen
    sim.simModel = new PropertySet( {showHomeScreen: showHomeScreen, screenIndex: options.screenIndex || 0 } );

    var $body = $( 'body' );
    $body.css( 'padding', '0' ).css( 'margin', '0' ).css( 'overflow', 'hidden' ); // prevent scrollbars

    //TODO should probably look for this div to see if it exists, then create only if it doesn't exist.
    //Add a div for the sim to the DOM
    var $simDiv = $( '<div>' ).attr( 'id', 'sim' ).css( 'position', 'absolute' ).css( 'left', 0 ).css( 'top', 0 );
    $body.append( $simDiv );
    this.$simDiv = $simDiv;

    //Create the scene
    //Leave accessibility as a flag while in development
    sim.scene = new Scene( $simDiv, {allowDevicePixelRatioScaling: false, accessible: true} );
    sim.scene.sim = sim; // add a reference back to the simulation
    sim.scene.initializeWindowEvents( { batchDOMEvents: true } ); // sets up listeners on the document with preventDefault(), and forwards those events to our scene
    if ( options.recordInputEventLog ) {
      sim.scene.input.logEvents = true; // flag Scenery to log all input events
    }
    window.simScene = sim.scene; // make the scene available for debugging

    var showPointers = window.phetcommon && window.phetcommon.getQueryParameter && window.phetcommon.getQueryParameter( 'showPointers' );

    this.showPointersProperty = new Property( showPointers );
    this.showPointersProperty.link( function( showPointers ) {
      sim.scene.setPointerDisplayVisible( showPointers );
    } );

    sim.navigationBar = new NavigationBar( sim, screens, sim.simModel );

    if ( screens.length > 1 ) {
      sim.homeScreen = new HomeScreen( sim );
    }

    //The simNode contains the home screen or the play area
    var simNode = new Node();

    //The viewContainer contains the ScreenView itself, which will be swapped out based on which icon the user selected in the navigation bar.
    //Without this layerSplit, the performance significantly declines on both Win8/Chrome and iPad3/Safari
    //TODO: Test this after rewriting into multiple divs/scenes
    var viewContainer = new Node( {layerSplit: true} );

    var updateBackground = function() {
      if ( sim.simModel.showHomeScreen ) {
        $simDiv.css( 'background', 'black' );
      }
      else {
        var backgroundColor = screens[sim.simModel.screenIndex].backgroundColor || 'white';
        var cssColor = ( typeof backgroundColor === 'string' ) ? backgroundColor : backgroundColor.toCSS();
        $simDiv.css( 'background', cssColor );
      }
    };

    //When the user presses the home icon, then show the homescreen, otherwise show the screen.
    sim.simModel.showHomeScreenProperty.link( function( showHomeScreen ) {
      simNode.children = showHomeScreen ? [] : [viewContainer];
      if ( showHomeScreen ) {
        sim.scene.children = [sim.homeScreen];
      }
      else {
        sim.scene.children = [simNode, sim.navigationBar];
      }
      updateBackground();
    } );

    //Instantiate the screens
    //Currently this is done eagerly, but this pattern leaves open the door for loading things in the background.
    _.each( screens, function( m ) {
      m.model = m.createModel();
      m.view = m.createView( m.model );
    } );

    //SR: ModuleIndex should always be defined.  On startup screenIndex=0 to highlight the 1st screen.
    //    When moving from a screen to the homescreen, the previous screen should be highlighted
    //When the user selects a different screen, show it.
    sim.simModel.screenIndexProperty.link( function( screenIndex ) {
      viewContainer.children = [screens[screenIndex].view];
      updateBackground();
    } );

    updateBackground();

    //Fit to the window and render the initial scene
    $( window ).resize( function() { sim.resizeToWindow(); } );
    sim.resizeToWindow();
  }

  Sim.prototype.resizeToWindow = function() {
    this.resize( window.innerWidth, window.innerHeight );
  };

  Sim.prototype.resize = function( width, height ) {
    var sim = this;

    //Use Mobile Safari layout bounds to size the home screen and navigation bar
    var scale = Math.min( width / 768, height / 504 );

    //40 px high on Mobile Safari
    var navBarHeight = scale * 40;
    sim.navigationBar.layout( scale, width, navBarHeight, height );
    sim.navigationBar.y = height - navBarHeight;
    sim.scene.resize( width, height );

    //Layout each of the screens
    _.each( sim.screens, function( m ) { m.view.layout( width, height - sim.navigationBar.height ); } );

    if ( sim.homeScreen ) {
      sim.homeScreen.layout( width, height );
    }
    //Startup can give spurious resizes (seen on ipad), so defer to the animation loop for painting

    sim.scene.input.eventLog.push( 'scene.sim.resize(' + width + ',' + height + ');' );
  };

  Sim.prototype.start = function() {
    var sim = this;

    // if the playback flag is set, don't start up like normal. instead download our event log from the server and play it back.
    // if direct playback (copy-paste) is desired, please directly call sim.startInputEventPlayback( ... ) instead of sim.start().
    if ( this.options.playbackInputEventLog ) {
      var request = new XMLHttpRequest();
      request.open( 'GET', this.getEventLogLocation(), true );
      request.onload = function( e ) {
        // we create functions, so eval is necessary. we go to the loaded domain on a non-standard port, so cross-domain issues shouldn't present themselves
        /* jshint -W061 */
        sim.startInputEventPlayback( eval( request.responseText ) );
      };
      request.send();
      return;
    }

    //Keep track of the previous time for computing dt, and initially signify that time hasn't been recorded yet.
    var lastTime = -1;

    //Make sure requestAnimationFrame is defined
    Util.polyfillRequestAnimationFrame();

    //Record the pointers (if logging is enabled)
//    var logPointers = new LogPointers();
//    logPointers.startLogging();
//
//    //For debugging, display the pointers
//    logPointers.showPointers();

    // place the rAF *before* the render() to assure as close to 60fps with the setTimeout fallback.
    //http://paulirish.com/2011/requestanimationframe-for-smart-animating/
    (function animationLoop() {
      var dt;

      // increment this before we can have an exception thrown, to see if we are missing frames
      sim.frameCounter++;

      if ( !sim.destroyed ) {
        window.requestAnimationFrame( animationLoop );
      }

      phetAllocation && phetAllocation( 'loop' );

      // fire or synthesize input events
      if ( sim.options.fuzzMouse ) {
        sim.fuzzMouseEvents();
      }
      else if ( sim.options.fuzzTouches ) {
        // TODO: we need more state tracking of individual touch points to do this properly
      }
      else {
        // if any input events were received and batched, fire them now.
        sim.scene.fireBatchedEvents();
      }

      //Update the active screen, but not if the user is on the home screen
      if ( !sim.simModel.showHomeScreen ) {

        //Compute the elapsed time since the last frame, or guess 1/60th of a second if it is the first frame
        var time = Date.now();
        var elapsedTimeMilliseconds = (lastTime === -1) ? (1000.0 / 60.0) : (time - lastTime);
        lastTime = time;

        //Convert to seconds
        dt = elapsedTimeMilliseconds / 1000.0;
        sim.screens[sim.simModel.screenIndex].model.step( dt );
      }

      //If using the TWEEN animation library, then update all of the tweens (if any) before rendering the scene.
      //Update the tweens after the model is updated but before the scene is redrawn.
      if ( window.TWEEN ) {
        window.TWEEN.update();
      }
      if ( sim.options.recordInputEventLog ) {
        // push a frame entry into our inputEventLog
        var entry = {
          dt: dt,
          events: sim.scene.input.eventLog,
          id: sim.frameCounter,
          time: Date.now()
        };
        if ( !sim.inputEventBounds.equals( sim.scene.sceneBounds ) ) {
          sim.inputEventBounds = sim.scene.sceneBounds.copy();

          entry.width = sim.scene.sceneBounds.width;
          entry.height = sim.scene.sceneBounds.height;
        }
        sim.inputEventLog.push( entry );
        sim.scene.input.eventLog = []; // clears the event log so that future actions will fill it
      }
      sim.scene.updateScene();
    })();
  };

  Sim.prototype.startPlayback = function( logArray ) {
    var sim = this;
    var logIndex = 0;
    var playbackTime = logArray[0].time;

    //Make sure requestAnimationFrame is defined
    Util.polyfillRequestAnimationFrame();

    //Display the pointers
//    new LogPointers().showPointers();
    var totalTime = 0;

    // place the rAF *before* the render() to assure as close to 60fps with the setTimeout fallback.
    //http://paulirish.com/2011/requestanimationframe-for-smart-animating/
    (function animationLoop() {
      if ( logIndex >= logArray.length ) {
        console.log( totalTime );

        sim.scene.addChild( new Text( 'Elapsed time (ms): ' + totalTime, {x: 100, y: 100, font: new PhetFont( 32 ) } ) );
        sim.scene.updateScene();
        return;
      }

      window.requestAnimationFrame( animationLoop );

      var start = Date.now();
      //Update the sim based on the given log
      logIndex = log.stepUntil( logArray, playbackTime, logIndex );

      playbackTime += 17;//ms between frames at 60fp

      sim.scene.updateScene();
      var stop = Date.now();
      var elapsed = (stop - start);

      totalTime += elapsed;
    })();
  };

  // Plays back input events and updateScene() loops based on recorded data. data should be an array of objects (representing frames) with dt and fireEvents( scene, dot )
  Sim.prototype.startInputEventPlayback = function( data ) {
    var sim = this;

    var index = 0; // our index into our frame data.

    //Make sure requestAnimationFrame is defined
    Util.polyfillRequestAnimationFrame();

    if ( data.length && data[0].width ) {
      sim.resize( data[0].width, data[0].height );
    }

    var startTime = Date.now();

    (function animationLoop() {
      var frame = data[index++];

      // when we have aready played the last frame
      if ( frame === undefined ) {
        var endTime = Date.now();

        var elapsedTime = endTime - startTime;
        var fps = data.length / ( elapsedTime / 1000 );

        // replace the page with a performance message
        document.body.innerHTML = '<div style="text-align: center; font-size: 16px;">' +
                                  '<h1>Performance results:</h1>' +
                                  '<p>Approximate frames per second: <strong>' + fps.toFixed( 1 ) + '</strong></p>' +
                                  '<p>Average time per frame (ms/frame): <strong>' + (elapsedTime / index).toFixed( 1 ) + '</strong></p>' +
                                  '<p>Elapsed time: <strong>' + elapsedTime + 'ms</strong></p>' +
                                  '<p>Number of frames: <strong>' + index + '</strong></p>' +
                                  '</div>';

        // ensure that the black text is readable (chipper-built sims have a black background right now)
        document.body.style.backgroundColor = '#fff';

        // bail before the requestAnimationFrame if we are at the end (stops the frame loop)
        return;
      }

      window.requestAnimationFrame( animationLoop );

      // we don't fire batched input events (prevents them from affecting unit/performance tests).
      // instead, we fire pre-recorded events for the scene if it exists (left out for brevity when not necessary)
      if ( frame.fireEvents ) { frame.fireEvents( sim.scene, function( x, y ) { return new Vector2( x, y ); } ); }

      //Update the active screen, but not if the user is on the home screen
      if ( !sim.simModel.showHomeScreen ) {
        sim.screens[sim.simModel.screenIndex].model.step( frame.dt ); // use the pre-recorded dt to ensure lack of variation between runs
      }

      //If using the TWEEN animation library, then update all of the tweens (if any) before rendering the scene.
      //Update the tweens after the model is updated but before the scene is redrawn.
      if ( window.TWEEN ) {
        window.TWEEN.update();
      }
      sim.scene.updateScene();
    })();
  };

  Sim.prototype.addChild = function( node ) {
    this.scene.addChild( node );
  };

  // A string that should be evaluated as JavaScript containing an array of "frame" objects, with a dt and an optional fireEvents function
  Sim.prototype.getRecordedInputEventLogString = function() {
    return '[\n' + _.map( this.inputEventLog,function( item ) {
      var fireEvents = 'fireEvents:function(scene,dot){' + _.map( item.events,function( str ) { return 'scene.input.' + str; } ).join( '' ) + '}';
      return '{dt:' + item.dt + ( item.events.length ? ',' + fireEvents : '' ) + ( item.width ? ',width:' + item.width : '' ) + ( item.height ? ',height:' + item.height : '' ) +
             ',id:' + item.id + ',time:' + item.time + '}';
    } ).join( ',\n' ) + '\n]';
  };

  // For recording and playing back input events, we use a unique combination of the user agent, width and height, so the same
  // server can test different recorded input events on different devices/browsers (desired, because events and coordinates are different)
  Sim.prototype.getEventLogName = function() {
    var name = this.options.inputEventLogName;
    if ( name === 'browser' ) {
      name = window.navigator.userAgent;
    }
    return ( this.name + '_' + name ).replace( /[^a-zA-Z0-9]/g, '_' );
  };

  // protocol-relative URL to the same-origin on a different port, for loading/saving recorded input events and frames
  Sim.prototype.getEventLogLocation = function() {
    var host = window.location.host.split( ':' )[0]; // grab the hostname without the port
    return '//' + host + ':8083/' + this.getEventLogName();
  };

  // submits a recorded event log to the same-origin server (run scenery/tests/event-logs/server/server.js with Node, from the same directory)
  Sim.prototype.submitEventLog = function() {
    // if we aren't recording data, don't submit any!
    if ( !this.options.recordInputEventLog ) { return; }

    var data = this.getRecordedInputEventLogString();

    var xmlhttp = new XMLHttpRequest();
    xmlhttp.open( 'POST', this.getEventLogLocation(), true ); // use a protocol-relative port to send it to Scenery's local event-log server
    xmlhttp.setRequestHeader( 'Content-type', 'text/javascript' );
    xmlhttp.send( data );
  };

  // submits a recorded event log to the same-origin server (run scenery/tests/event-logs/server/server.js with Node, from the same directory)
  Sim.prototype.mailEventLog = function() {
    // if we aren't recording data, don't submit any!
    if ( !this.options.recordInputEventLog ) { return; }

    var data = this.getRecordedInputEventLogString();

    window.open( 'mailto:phethelp@colorado.edu?subject=' + encodeURIComponent( this.name + ' input event log at ' + Date.now() ) + '&body=' + encodeURIComponent( data ) );
  };

  Sim.prototype.fuzzMouseEvents = function() {
    var sim = this;

    var chance;
    // run a variable number of events, with a certain chance of bailing out (so no events are possible)
    // models a geometric distribution of events
    while ( ( chance = Math.random() ) < 1 - 1 / sim.fuzzMouseAverage ) {
      var domEvent;
      if ( chance < ( sim.fuzzMouseLastMoved ? 0.02 : 0.4 ) ) {
        // toggle up/down
        domEvent = document.createEvent( 'MouseEvent' ); // not 'MouseEvents' according to DOM Level 3 spec

        // technically deprecated, but DOM4 event constructors not out yet. people on #whatwg said to use it
        domEvent.initMouseEvent( sim.fuzzMouseIsDown ? 'mouseup' : 'mousedown', true, true, window, 1, // click count
          sim.fuzzMousePosition.x, sim.fuzzMousePosition.y, sim.fuzzMousePosition.x, sim.fuzzMousePosition.y,
          false, false, false, false,
          0, // button
          null );

        sim.scene.input.validatePointers();

        if ( sim.fuzzMouseIsDown ) {
          sim.scene.input.mouseUp( sim.fuzzMousePosition, domEvent );
          sim.fuzzMouseIsDown = false;
        }
        else {
          sim.scene.input.mouseDown( sim.fuzzMousePosition, domEvent );
          sim.fuzzMouseIsDown = true;
        }
      }
      else {
        // change the mouse position
        sim.fuzzMousePosition = new Vector2(
          Math.floor( Math.random() * sim.scene.sceneBounds.width ),
          Math.floor( Math.random() * sim.scene.sceneBounds.height )
        );

        // our move event
        domEvent = document.createEvent( 'MouseEvent' ); // not 'MouseEvents' according to DOM Level 3 spec

        // technically deprecated, but DOM4 event constructors not out yet. people on #whatwg said to use it
        domEvent.initMouseEvent( 'mousemove', true, true, window, 0, // click count
          sim.fuzzMousePosition.x, sim.fuzzMousePosition.y, sim.fuzzMousePosition.x, sim.fuzzMousePosition.y,
          false, false, false, false,
          0, // button
          null );

        sim.scene.input.validatePointers();
        sim.scene.input.mouseMove( sim.fuzzMousePosition, domEvent );
      }
    }
  };

  //Destroy a sim so that it will no longer consume any resources.  Used by sim nesting in Smorgasbord
  Sim.prototype.destroy = function() {
    this.destroyed = true;
    this.$simDiv.remove();
  };

  return Sim;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model of a simple beaker.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/Beaker',[], function() {
  

  /**
   * Constructor
   * @param {Vector2} location bottom center
   * @param {Dimension2} size
   * @param {Number} volume in liters (L)
   * @constructor
   */
  function Beaker( location, size, volume ) {
    this.location = location;
    this.size = size;
    this.volume = volume;
  }

  Beaker.prototype = {

    reset: function() {
      // currently nothing to reset
    },

    // Gets the x-coordinate of the left wall.
    getLeft: function() {
      return this.location.x - ( this.size.width / 2 );
    },

    // Gets the x-coordinate of the right wall.
    getRight: function() {
      return this.location.x + ( this.size.width / 2 );
    }
  };

  return Beaker;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * A movable model element.
 * Semantics of units are determined by the client.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/common/model/Movable',['require','AXON/Property'], function( require ) {
  

  // imports
  var Property = require( 'AXON/Property' );

  /**
   * Constructor
   * @param {Vector2} location
   * @param {Bounds2} dragBounds
   * @constructor
   */
  function Movable( location, dragBounds ) {
    this.location = new Property( location );
    this.dragBounds = dragBounds;
  }

  Movable.prototype = {
    reset: function() {
      this.location.reset();
    }
  };

  return Movable;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model of the concentration meter.
 * <p/>
 * NOTE: Determining when the probe is in one of the various fluids is handled in the view,
 * where testing node intersections simplifies the process. Otherwise we'd need to
 * model the shapes of the various fluids, an unnecessary complication.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

define('BEERS_LAW_LAB/concentration/model/ConcentrationMeter',['require','BEERS_LAW_LAB/common/model/Movable','AXON/Property'], function( require ) {
  

  // imports
  var Movable = require( 'BEERS_LAW_LAB/common/model/Movable' );
  var Property = require( 'AXON/Property' );

  /**
   * @param {Vector2} bodyLocation
   * @param {Bounds2} bodyDragBounds
   * @param {Vector2} probeLocation
   * @param {Bounds2} probeDragBounds
   * @constructor
   */
  function ConcentrationMeter( bodyLocation, bodyDragBounds, probeLocation, probeDragBounds ) {
    this.value = new Property( NaN ); // NaN if the meter is not reading a value
    this.body = new Movable( bodyLocation, bodyDragBounds );
    this.probe = new Movable( probeLocation, probeDragBounds );
  }

  ConcentrationMeter.prototype = {
    reset: function() {
      this.value.reset();
      this.body.reset();
      this.probe.reset();
    }
  };

  return ConcentrationMeter;

} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Base type for all fluids.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/common/model/Fluid',['require','AXON/Property'], function( require ) {
  

  // imports
  var Property = require( 'AXON/Property' );

  /**
   * @param {Color} color
   * @constructor
   */
  function Fluid( color ) {
    this.color = new Property( color );
  }

  Fluid.prototype = {
    reset: function() {
      this.color.reset();
    }
  };

  return Fluid;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Miscellaneous chemistry functions.
 */
define('NITROGLYCERIN/ChemUtils',[], function() {
  

  function ChemUtils() {}
  
  /*
   * Creates a symbol (HTML fragment) based on the list of atoms in the molecule.
   * The atoms must be specified in order of appearance in the symbol.
   * Examples:
   *    [C,C,H,H,H,H] becomes "C<sub>2</sub>H<sub>4</sub>"
   *    [H,H,O] becomes "H<sub>2</sub>O"
   */
  ChemUtils.createSymbol = function( elements ) {
    return ChemUtils.toSubscript( ChemUtils.createSymbolWithoutSubscripts( elements ) );
  };
  
  /*
   * Creates a symbol (text) based on the list of atoms in the molecule.
   * The atoms must be specified in order of appearance in the symbol.
   * Examples:
   *    [C,C,H,H,H,H] becomes "C2H4"
   *    [H,H,O] becomes "H2O"
   */
  ChemUtils.createSymbolWithoutSubscripts = function( elements ) {
    var result = '';
    var atomCount = 1;
    var length = elements.length;
    for ( var i = 0; i < length; i++ ) {
      if ( i === 0 ) {
        // first atom is treated differently
        result += elements[i].symbol;
      } else if ( elements[i] === elements[i - 1] ) {
        // this atom is the same as the previous atom
        atomCount++;
      } else {
        // this atom is NOT the same
        if ( atomCount > 1 ) {
          // create a subscript
          result += atomCount;
        }
        atomCount = 1;
        result += elements[i].symbol;
      }
    }
    if ( atomCount > 1 ) {
      // create a subscript for the final atom
      result += atomCount;
    }
    return result;
  };
  
  /**
   * Return an integer that can be used for sorting atom symbols alphabetically. Lower values will be returned for
   * symbols that should go first. Two-letter symbols will come after a one-letter symbol with the same first
   * character (Br after B). See http://en.wikipedia.org/wiki/Hill_system, for without carbon
   *
   * @param {Element} element An element
   * @return {Number} Value for sorting
   */
  ChemUtils.nonCarbonHillSortValue = function( element ) {
    // TODO: if it's a performance issue, we should put these in Element itself
    // yes, will totally fail if our Unicode code point of the 2nd character is >1000. Agile coding? We like to live on the edge
    var value = 1000 * element.symbol.charCodeAt( 0 );
    if ( element.symbol.length > 1 ) {
      value += element.symbol.charCodeAt( 1 );
    }
    return value;
  };
  
  /**
   * Returns an integer that can be used for sorting atom symbols for the Hill system when the molecule contains
   * carbon. See http://en.wikipedia.org/wiki/Hill_system
   *
   * @param {Element} element An element
   * @return {Number} Value for sorting (lowest is first)
   */
  ChemUtils.carbonHillSortValue = function( element ) {
    // TODO: if it's a performance issue, we should put these in Element itself
    if ( element.isCarbon() ) {
      return 0;
    } else if ( element.isHydrogen() ) {
      return 1;
    } else {
      return ChemUtils.nonCarbonHillSortValue( element );
    }
  };

  /**
   * Handles HTML subscript formatting for molecule symbols.
   * All numbers in a string are assumed to be part of a subscript, and will be enclosed in a <sub> tag.
   * For example, 'C2H4' becomes 'C<sub>2</sub>H<sub>4</sub>'.
   * @param {String} inString the input plaintext string
   * @return {String} HTML fragment
   */
  ChemUtils.toSubscript = function( inString ) {
    var outString = '';
    var sub = false; // are we in a <sub> tag?
    var isDigit = function( c ) {
      return ( c >= '0' && c <= '9');
    };
    for ( var i = 0; i < inString.length; i++ ) {
      var c = inString.charAt( i );
      if ( !sub && isDigit( c ) ) {
        // start the subscript tag when a digit is found
        outString += '<sub>';
        sub = true;
      }
      else if ( sub && !isDigit( c ) ) {
        // end the subscript tag when a non-digit is found
        outString += '</sub>';
        sub = false;
      }
      outString += c;
    }
    // end the subscript tag if inString ends with a digit
    if ( sub ) {
      outString += '</sub>';
      sub = false;
    }
    return outString;
  };
  
  /**
   * @param atoms A collection of atoms in a molecule. NOTE: in Java, they were Element objects! 
   * @return The molecular formula of the molecule in the Hill system. Returned as an HTML fragment. See
   *         http://en.wikipedia.org/wiki/Hill_system for more information.
   */
  ChemUtils.hillOrderedSymbol = function( atoms ) {
    var containsCarbon = _.some( atoms, function( atom ) { return atom.isCarbon(); } );
    var sortedAtoms = _.sortBy( atoms, containsCarbon ?
                                       ChemUtils.carbonHillSortValue :  // carbon first, then hydrogen, then others alphabetically
                                       ChemUtils.nonCarbonHillSortValue // compare alphabetically since there is no carbon
                                       );
    return ChemUtils.createSymbol( sortedAtoms );
  };

  return ChemUtils;
} );

/**
 * @license RequireJS i18n 2.0.4 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
 * Available via the MIT or new BSD license.
 * see: http://github.com/requirejs/i18n for details
 */
/*jslint regexp: true */
/*global require: false, navigator: false, define: false */

/**
 * This plugin handles i18n! prefixed modules. It does the following:
 *
 * 1) A regular module can have a dependency on an i18n bundle, but the regular
 * module does not want to specify what locale to load. So it just specifies
 * the top-level bundle, like "i18n!nls/colors".
 *
 * This plugin will load the i18n bundle at nls/colors, see that it is a root/master
 * bundle since it does not have a locale in its name. It will then try to find
 * the best match locale available in that master bundle, then request all the
 * locale pieces for that best match locale. For instance, if the locale is "en-us",
 * then the plugin will ask for the "en-us", "en" and "root" bundles to be loaded
 * (but only if they are specified on the master bundle).
 *
 * Once all the bundles for the locale pieces load, then it mixes in all those
 * locale pieces into each other, then finally sets the context.defined value
 * for the nls/colors bundle to be that mixed in locale.
 *
 * 2) A regular module specifies a specific locale to load. For instance,
 * i18n!nls/fr-fr/colors. In this case, the plugin needs to load the master bundle
 * first, at nls/colors, then figure out what the best match locale is for fr-fr,
 * since maybe only fr or just root is defined for that locale. Once that best
 * fit is found, all of its locale pieces need to have their bundles loaded.
 *
 * Once all the bundles for the locale pieces load, then it mixes in all those
 * locale pieces into each other, then finally sets the context.defined value
 * for the nls/fr-fr/colors bundle to be that mixed in locale.
 */
(function () {
  

  //regexp for reconstructing the master bundle name from parts of the regexp match
  //nlsRegExp.exec("foo/bar/baz/nls/en-ca/foo") gives:
  //["foo/bar/baz/nls/en-ca/foo", "foo/bar/baz/nls/", "/", "/", "en-ca", "foo"]
  //nlsRegExp.exec("foo/bar/baz/nls/foo") gives:
  //["foo/bar/baz/nls/foo", "foo/bar/baz/nls/", "/", "/", "foo", ""]
  //so, if match[5] is blank, it means this is the top bundle definition.
  var nlsRegExp = /(^.*(^|\/)nls(\/|$))([^\/]*)\/?([^\/]*)/;

  //Helper function to avoid repeating code. Lots of arguments in the
  //desire to stay functional and support RequireJS contexts without having
  //to know about the RequireJS contexts.
  function addPart(locale, master, needed, toLoad, prefix, suffix) {
    if (master[locale]) {
      needed.push(locale);
      if (master[locale] === true || master[locale] === 1) {
        toLoad.push(prefix + locale + '/' + suffix);
      }
    }
  }

  function addIfExists(req, locale, toLoad, prefix, suffix) {
    var fullName = prefix + locale + '/' + suffix;
    if (require._fileExists(req.toUrl(fullName + '.js'))) {
      toLoad.push(fullName);
    }
  }

  /**
   * Simple function to mix in properties from source into target,
   * but only if target does not already have a property of the same name.
   * This is not robust in IE for transferring methods that match
   * Object.prototype names, but the uses of mixin here seem unlikely to
   * trigger a problem related to that.
   */
  function mixin(target, source, force) {
    var prop;
    for (prop in source) {
      if (source.hasOwnProperty(prop) && (!target.hasOwnProperty(prop) || force)) {
        target[prop] = source[prop];
      } else if (typeof source[prop] === 'object') {
        if (!target[prop] && source[prop]) {
          target[prop] = {};
        }
        mixin(target[prop], source[prop], force);
      }
    }
  }

  define('i18n',['module'], function (module) {
    var masterConfig = module.config ? module.config() : {};

    return {
      version: '2.0.4',
      /**
       * Called when a dependency needs to be loaded.
       */
      load: function (name, req, onLoad, config) {
        config = config || {};

        if (config.locale) {
          masterConfig.locale = config.locale;
        }

        var masterName,
          match = nlsRegExp.exec(name),
          prefix = match[1],
          locale = match[4],
          suffix = match[5],
          parts = locale.split("-"),
          toLoad = [],
          value = {},
          i, part, current = "";

        //If match[5] is blank, it means this is the top bundle definition,
        //so it does not have to be handled. Locale-specific requests
        //will have a match[4] value but no match[5]
        if (match[5]) {
          //locale-specific bundle
          prefix = match[1];
          masterName = prefix + suffix;
        } else {
          //Top-level bundle.
          masterName = name;
          suffix = match[4];
          locale = masterConfig.locale;
          if (!locale) {
            locale = masterConfig.locale =
                     typeof navigator === "undefined" ? "root" :
                     (navigator.language ||
                      navigator.userLanguage || "root").toLowerCase();
          }
          parts = locale.split("-");
        }

        if (config.isBuild) {
          //Check for existence of all locale possible files and
          //require them if exist.
          toLoad.push(masterName);
          addIfExists(req, "root", toLoad, prefix, suffix);
          for (i = 0; i < parts.length; i++) {
            part = parts[i];
            current += (current ? "-" : "") + part;
            addIfExists(req, current, toLoad, prefix, suffix);
          }

          req(toLoad, function () {
            onLoad();
          });
        } else {
          //First, fetch the master bundle, it knows what locales are available.
          req([masterName], function (master) {
            //Figure out the best fit
            var needed = [],
              part;

            //Always allow for root, then do the rest of the locale parts.
            addPart("root", master, needed, toLoad, prefix, suffix);
            for (i = 0; i < parts.length; i++) {
              part = parts[i];
              current += (current ? "-" : "") + part;
              addPart(current, master, needed, toLoad, prefix, suffix);
            }

            //Load all the parts missing.
            req(toLoad, function () {
              var i, partBundle, part;
              for (i = needed.length - 1; i > -1 && needed[i]; i--) {
                part = needed[i];
                partBundle = master[part];
                if (partBundle === true || partBundle === 1) {
                  partBundle = req(prefix + part + '/' + suffix);
                }
                mixin(value, partBundle);
              }

              //All done, notify the loader.
              onLoad(value);
            });
          });
        }
      }
    };
  });
}());
// Copyright 2002-2013, University of Colorado

define('BEERS_LAW_LAB/../nls/beers-law-lab-strings', {
  "root": true
} );
// Copyright 2002-2013, University of Colorado

define('BEERS_LAW_LAB/../nls/root/beers-law-lab-strings', {
  "absorbance": "Absorbance",
  "beersLaw": "Beer's Law",
  "beersLawLab": "Beer's Law Lab",
  "cobaltIINitrate": "Cobalt (II) nitrate",
  "cobaltChloride": "Cobalt chloride",
  "concentration": "Concentration",
  "copperSulfate": "Copper sulfate",
  "drinkMix": "Drink mix",
  "evaporation": "Evaporation",
  "preset": "preset",
  "ir": "IR",
  "lots": "lots",
  "nickelIIChloride": "Nickel (II) chloride",
  "none": "none",
  "pattern_0label": "{0}:",
  "pattern_0value_1units": "{0} {1}",
  "pattern_parentheses_0text": "({0})",
  "pattern_0percent": "{0}%",
  "pattern_0formula_1name": "{0}: {1}",
  "potassiumChromate": "Potassium chromate",
  "potassiumDichromate": "Potassium dichromate",
  "potassiumPermanganate": "Potassium permanganate",
  "removeSolute": "Remove Solute",
  "saturated": "Saturated!",
  "solid": "Solid",
  "solute": "Solute",
  "solution": "Solution",
  "transmittance": "Transmittance",
  "units_centimeters": "cm",
  "units_liters": "L",
  "units_molesPerLiter": "mol/L",
  "units_mM": "mM",
  "units_uM": "\u00b5M",
  "units_nm": "nm",
  "uv": "UV",
  "variable": "variable",
  "water": "water",
  "wavelength": "Wavelength"
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Load the strings only once and from a single location, because loading them from different relative paths
 * causes them to fall back to the root language sometimes for unknown reasons.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beers-law-lab-strings',['require','i18n!BEERS_LAW_LAB/../nls/beers-law-lab-strings'], function( require ) {
  
  return require( 'i18n!BEERS_LAW_LAB/../nls/beers-law-lab-strings' );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Universal chemical symbols, no i18n needed.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/common/BLLSymbols',['require','NITROGLYCERIN/ChemUtils','BEERS_LAW_LAB/beers-law-lab-strings'], function( require ) {
  

  // imports
  var toSubscript = require( 'NITROGLYCERIN/ChemUtils' ).toSubscript;
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );

  return {
    COBALT_II_NITRATE: toSubscript( 'Co(NO3)2' ),
    COBALT_CHLORIDE: toSubscript( 'CoCl2' ),
    COPPER_SULFATE: toSubscript( 'CuSO4' ),
    DRINK_MIX: strings.drinkMix,
    NICKEL_II_CHLORIDE: toSubscript( 'NiCl2' ),
    POTASSIUM_CHROMATE: toSubscript( 'K2CrO4' ),
    POTASSIUM_DICHROMATE: toSubscript( 'K2Cr2O7' ),
    POTASSIUM_PERMANGANATE: toSubscript( 'KMnO4' ),
    WATER: toSubscript( 'H2O' )
  };
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 *  A solvent (in this case a liquid) that dissolves another liquid (the solute) to create a solution.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/common/model/Solvent',['require','BEERS_LAW_LAB/common/BLLSymbols','SCENERY/util/Color','BEERS_LAW_LAB/common/model/Fluid','PHET_CORE/inherit','BEERS_LAW_LAB/beers-law-lab-strings'], function( require ) {
  

  // imports
  var BLLSymbols = require( 'BEERS_LAW_LAB/common/BLLSymbols' );
  var Color = require( 'SCENERY/util/Color' );
  var Fluid = require( 'BEERS_LAW_LAB/common/model/Fluid' );
  var inherit = require( 'PHET_CORE/inherit' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );

  /**
   * Constructor.
   * @param {String} name
   * @param {String} formula
   * @param {Color} color
   * @constructor
   */
  function Solvent( name, formula, color ) {

    Fluid.call( this, color );

    this.name = name;
    this.formula = formula;
  }

  inherit( Fluid, Solvent );

  Solvent.WATER_COLOR = new Color( 224, 255, 255 );

  Solvent.WATER = new Solvent( strings.water, BLLSymbols.WATER, Solvent.WATER_COLOR );

  return Solvent;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Solution model for the 'Concentration' screen.
 * This screen has a single solution that is mutated by changing the solute, solute amount, and volume.
 * Concentration is derived via M=mol/L.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/ConcentrationSolution',['require','SCENERY/util/Color','BEERS_LAW_LAB/common/model/Fluid','PHET_CORE/inherit','AXON/Property','BEERS_LAW_LAB/common/model/Solvent'], function( require ) {
  

  // imports
  var Color = require( 'SCENERY/util/Color' );
  var Fluid = require( 'BEERS_LAW_LAB/common/model/Fluid' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Property = require( 'AXON/Property' );
  var Solvent = require( 'BEERS_LAW_LAB/common/model/Solvent' );

  /**
   * @param {Property<Solute>} solute
   * @param {Number} soluteAmount moles
   * @param {NUmber} volume L
   */
  function ConcentrationSolution( solute, soluteAmount, volume ) {

    var thisSolution = this;
    Fluid.call( thisSolution, Color.WHITE ); // use a bogus initial color, so we'll need to set color properly in reset

    thisSolution.solvent = Solvent.WATER;
    thisSolution.solute = solute;
    thisSolution.soluteAmount = new Property( soluteAmount );
    thisSolution.volume = new Property( volume );

    // derive amount of precipitate (moles)
    thisSolution.precipitateAmount = new Property( 0 );
    thisSolution.concentration = new Property( 0 );
    var updatePrecipitateAmount = function() {

      // derive amount of precipitate (moles)
      var volume = thisSolution.volume.get();
      if ( volume > 0 ) {
        thisSolution.precipitateAmount.set( Math.max( 0, volume * ( ( thisSolution.soluteAmount.get() / volume ) - thisSolution.getSaturatedConcentration() ) ) );
      }
      else {
        thisSolution.precipitateAmount.set( thisSolution.soluteAmount.get() );
      }

      // derive concentration (M)
      var soluteAmount = thisSolution.soluteAmount.get();
      var concentration = ( volume > 0 ) ? Math.min( thisSolution.getSaturatedConcentration(), soluteAmount / volume ) : 0; // M = mol/L
      thisSolution.concentration.set( concentration );
    };
    thisSolution.solute.link( updatePrecipitateAmount );
    thisSolution.soluteAmount.link( updatePrecipitateAmount );
    thisSolution.volume.link( updatePrecipitateAmount );

    // derive the solution color
    var updateColor = function() {
      thisSolution.color.set( ConcentrationSolution.createColor( thisSolution.solvent, thisSolution.solute.get(), thisSolution.concentration.get() ) );
    };
    thisSolution.solute.link( updateColor );
    thisSolution.concentration.link( updateColor );

    // reset
    thisSolution.reset = function() {
      Fluid.prototype.reset.call( this );
      thisSolution.soluteAmount.reset();
      thisSolution.volume.reset();
      updateColor(); // because we provided a bogus initial color to Fluid constructor
    };
  }

  inherit( Fluid, ConcentrationSolution, {

    // convenience function
    getSaturatedConcentration: function() {
      return this.solute.get().getSaturatedConcentration();
    },

    isSaturated: function() {
      var saturated = false;
      if ( this.volume.get() > 0 ) {
        saturated = ( this.soluteAmount.get() / this.volume.get() ) > this.getSaturatedConcentration();
      }
      return saturated;
    },

    getNumberOfPrecipitateParticles: function() {
      var numberOfParticles = Math.round( this.solute.get().particlesPerMole * this.precipitateAmount.get() );
      if ( numberOfParticles === 0 && this.precipitateAmount.get() > 0 ) {
        numberOfParticles = 1;
      }
      return numberOfParticles;
    }
  } );

  /*
   * Creates a color that corresponds to the solution's concentration.
   * @param {Solvent) solvent
   * @param {Solute} solute
   * @param {Number} concentration
   */
  ConcentrationSolution.createColor = function( solvent, solute, concentration ) {
    var color = solvent.color.get();
    if ( concentration > 0 ) {
      color = solute.colorScheme.concentrationToColor( concentration );
    }
    return color;
  };

  return ConcentrationSolution;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Basic width and height
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('DOT/Dimension2',['require','DOT/dot'], function( require ) {
  
  
  var dot = require( 'DOT/dot' );
  
  dot.Dimension2 = function Dimension2( width, height ) {
    this.width = width;
    this.height = height;
  };
  var Dimension2 = dot.Dimension2;

  Dimension2.prototype = {
    constructor: Dimension2,

    toString: function() {
      return "[" + this.width + "w, " + this.height + "h]";
    },

    equals: function( other ) {
      return this.width === other.width && this.height === other.height;
    }
  };
  
  return Dimension2;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model of the dropper, contains solute in solution form (stock solution).
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/Dropper',['require','PHET_CORE/inherit','BEERS_LAW_LAB/common/model/Movable','AXON/Property'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Movable = require( 'BEERS_LAW_LAB/common/model/Movable' );
  var Property = require( 'AXON/Property' );

  /**
   * @param {Vector2} location
   * @param {Bounds2} dragBounds
   * @param {Property<Solute>} solute
   * @param {Number} maxFlowRate
   * @param {Boolean} visible
   * @constructor
   */
  function Dropper( location, dragBounds, solute, maxFlowRate, visible ) {

    var thisDropper = this;
    Movable.call( thisDropper, location, dragBounds );

    thisDropper.solute = solute;
    thisDropper.visible = new Property( visible );
    thisDropper.on = new Property( false ); // true if the dropper is dispensing solution
    thisDropper.enabled = new Property( true );
    thisDropper.empty = new Property( false );
    thisDropper.flowRate = new Property( 0 ); // L/sec

    // Turn off the dropper when it's disabled.
    thisDropper.enabled.link( function( enabled ) {
      if ( !enabled ) {
        thisDropper.on.set( false );
      }
    } );

    // Toggle the flow rate when the dropper is turned on/off.
    thisDropper.on.link( function( on ) {
      thisDropper.flowRate.set( on ? maxFlowRate : 0 );
    } );

    // When the dropper becomes empty, disable it.
    thisDropper.empty.link( function( empty ) {
      if ( empty ) {
        thisDropper.enabled.set( false );
      }
    } );
  }

  inherit( Movable, Dropper, {
    reset: function() {
      Movable.prototype.reset.call( this );
      this.visible.reset();
      this.on.reset();
      this.enabled.reset();
      this.empty.reset();
      this.flowRate.reset();
    }
  } );

  return Dropper;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model element that determines the rate at which solvent is evaporated from the solution in the beaker.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/Evaporator',['require','AXON/Property'], function( require ) {
  

  // imports
  var Property = require( 'AXON/Property' );

  /**
   * @param {Number} maxEvaporationRate L/sec
   * @param {ConcentrationSolution} solution
   * @constructor
   */
  function Evaporator( maxEvaporationRate, solution ) {

    var thisEvaporator = this;

    thisEvaporator.maxEvaporationRate = maxEvaporationRate; // L/sec
    thisEvaporator.evaporationRate = new Property( 0 ); // L/sec
    thisEvaporator.enabled = new Property( true );

    // disable when the volume gets to zero
    solution.volume.link( function( volume ) {
      thisEvaporator.enabled.set( volume > 0 );
    } );

    // when disabled, set the rate to zero
    thisEvaporator.enabled.link( function( enabled ) {
      if ( !enabled ) {
        thisEvaporator.evaporationRate.set( 0 );
      }
    } );
  }

  Evaporator.prototype = {
    reset: function() {
      this.evaporationRate.reset();
      this.enabled.reset();
    }
  };

  return Evaporator;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Faucet model, used for input and output faucets.
 * This model assumes that the pipe enters the faucet from the left.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/Faucet',['require','ASSERT/assert','AXON/Property'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var Property = require( 'AXON/Property' );

  /**
   * @param {Vector2} location center of output pipe
   * @param {Number} pipeMinX x-coordinate of where the pipe starts
   * @param {Number} spoutWidth
   * @param {Number} maxFlowRate L/sec
   * @constructor
   */
  function Faucet( location, pipeMinX, spoutWidth, maxFlowRate ) {

    assert && assert( pipeMinX < location.x ); // pipe enters the faucet from the left

    var thisFaucet = this;

    thisFaucet.location = location;
    thisFaucet.pipeMinX = pipeMinX;
    thisFaucet.spoutWidth = spoutWidth;
    thisFaucet.maxFlowRate = maxFlowRate;
    thisFaucet.flowRate = new Property( 0 );
    thisFaucet.enabled = new Property( true );

    // when disabled, turn off the faucet.
    thisFaucet.enabled.link( function( enabled ) {
      if ( !enabled ) {
        thisFaucet.flowRate.set( 0 );
      }
    } );
  }

  Faucet.prototype = {
    reset: function() {
      this.flowRate.reset();
      this.enabled.reset();
    }
  };

  return Faucet;
} );


// Copyright 2002-2013, University of Colorado Boulder

/**
 * Base class for solute particles.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/SoluteParticle',['require','AXON/Property'], function( require ) {
  

  // imports
  var Property = require( 'AXON/Property' );

  /**
   * Constructor
   * @param {Color} color
   * @param {Number} size particles are square, this is the length of one side
   * @param {Vector2} location location of the particle in the beaker's coordinate frame
   * @param {Number} orientation in radians
   * @constructor
   */
  function SoluteParticle( color, size, location, orientation ) {
    this.color = color;
    this.size = size;
    this.location = new Property( location );
    this.orientation = orientation;
  }

  return SoluteParticle;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * One particle that makes up the precipitate that forms on the bottom of the beaker.
 * Precipitate particles are static (they don't move). They have no associated animation,
 * and they magically appear on the bottom of the beaker.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/PrecipitateParticle',['require','PHET_CORE/inherit','BEERS_LAW_LAB/concentration/model/SoluteParticle'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var SoluteParticle = require( 'BEERS_LAW_LAB/concentration/model/SoluteParticle' );

  /**
   * Constructor
   * @param {Solute} solute
   * @param {Vector2} location location in the beaker's coordinate frame
   * @param {Number} orientation in radians
   * @constructor
   */
  function PrecipitateParticle( solute, location, orientation ) {
    SoluteParticle.call( this, solute.particleColor, solute.particleSize, location, orientation );
  }

  inherit( SoluteParticle, PrecipitateParticle );

  return PrecipitateParticle;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * The precipitate that forms on the bottom of the beaker.
 * Manages the creation and deletion of precipitate particles.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/Precipitate',['require','ASSERT/assert','BEERS_LAW_LAB/concentration/model/PrecipitateParticle','DOT/Vector2'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'graphing-lines' );
  var PrecipitateParticle = require( 'BEERS_LAW_LAB/concentration/model/PrecipitateParticle' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * @param {ConcentrationSolution} solution
   * @param {Beaker} beaker
   * @constructor
   */
  function Precipitate( solution, beaker ) {

    var thisPrecipitate = this;

    thisPrecipitate.solution = solution;
    thisPrecipitate.beaker = beaker;
    thisPrecipitate.particles = []; // PrecipitateParticle
    thisPrecipitate.changedCallbacks = []; // function(Precipitate), private

    // when the saturation changes, update the number of precipitate particles
    thisPrecipitate.solution.precipitateAmount.link( function() {
      thisPrecipitate._updateParticles();
    } );

    // when the solute changes, remove all particles and create new particles for the solute
    thisPrecipitate.solution.solute.link( function() {
      thisPrecipitate._removeAllParticles();
      thisPrecipitate._updateParticles();
    } );
  }

  Precipitate.prototype = {
    /**
     * Registers a callback that will be notified when the precipitate changes.
     * @param {Precipitate~Callback} callback has a {Particle} parameter
     */
    registerChangedCallback: function( callback ) {
      this.changedCallbacks.push( callback );
    },

    /*
     * Adds/removes particles to match the model.
     * To optimize performance, clients who register for the 'change' callback will assume that
     * particles are added/removed from the end of the 'particles' array.  See #48.
     */
    _updateParticles: function() {
      var numberOfParticles = this.solution.getNumberOfPrecipitateParticles(); // number of particles desired after this update
      if ( numberOfParticles === this.particles.length ) {
        // no change, do nothing
        return;
      }
      else if ( numberOfParticles === 0 ) {
        // remove all particles
        this._removeAllParticles();
      }
      else if ( numberOfParticles < this.particles.length ) {
        // remove some particles
        var numberToRemove = this.particles.length - numberOfParticles;
        this.particles.splice( this.particles.length - 1 - numberToRemove, numberToRemove );
      }
      else {
        // add some particles
        while ( numberOfParticles > this.particles.length ) {
          this.particles.push( new PrecipitateParticle( this.solution.solute.get(), this._getRandomOffset(), getRandomOrientation() ) );
        }
      }
      assert && assert( this.particles.length === numberOfParticles );
      this._fireChanged();
    },

    _removeAllParticles: function() {
      this.particles = [];
    },

    // Notify that the precipitate has changed.
    _fireChanged: function() {
      var changedCallbacks = this.changedCallbacks.slice( 0 ); // copy to prevent concurrent modification
      for ( var i = 0; i < changedCallbacks.length; i++ ) {
        changedCallbacks[i]( this );
      }
    },

    // Gets a random location, in global model coordinate frame.
    _getRandomOffset: function() {
      var particleSize = this.solution.solute.get().particleSize;
      // particles are square, largest margin required is the diagonal length
      var margin = Math.sqrt( particleSize * particleSize );
      // offset
      var x = this.beaker.location.x - ( this.beaker.size.width / 2 ) + margin + ( Math.random() * ( this.beaker.size.width - ( 2 * margin ) ) );
      var y = this.beaker.location.y - margin; // this was tweaked based on the lineWidth used to stroke the beaker
      return new Vector2( x, y );
    }
  };

  // Gets a random orientation, in radians.
  var getRandomOrientation = function() {
    return Math.random() * 2 * Math.PI;
  };

  return Precipitate;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * A numeric range.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('DOT/Range',['require','ASSERT/assert','DOT/dot'], function ( require ) {
  

  var assert = require( 'ASSERT/assert' )( 'dot' );

  var dot = require( 'DOT/dot' );

  dot.Range = function Range( min, max, defaultValue ) {

    this.min = min;
    this.max = max;
    this.defaultValue = ( defaultValue === undefined ) ? min : defaultValue;

    assert && assert( min <= max );
    assert && assert( ( this.defaultValue >= min ) && ( this.defaultValue <= max ) );
  };
  var Range = dot.Range;

  Range.prototype = {

    constructor: Range,

    copy: function() {
       return new Range( this.min, this.max, this.defaultValue );
    },

    getLength: function () {
      return this.max - this.min;
    },

    contains: function ( value ) {
      return ( value >= this.min ) && ( value <= this.max );
    },

    intersects: function ( range ) {
      return ( this.max >= range.min ) && ( range.max >= this.min );
    },

    toString: function () {
      return "[Range (min:" + this.min + " max:" + this.max + " defaultValue:" + this.defaultValue + ")]";
    }
  };

  return Range;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model of a shaker, contains solute in solid form.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/Shaker',['require','PHET_CORE/inherit','BEERS_LAW_LAB/common/model/Movable','AXON/Property','DOT/Vector2'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Movable = require( 'BEERS_LAW_LAB/common/model/Movable' );
  var Property = require( 'AXON/Property' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * Constructor
   * @param {Vector2} location
   * @param {Number} orientation in radians
   * @param {Bounds2} dragBounds
   * @param {Property<Solute>} solute
   * @param {Number} maxDispensingRate
   * @param {Boolean} visible
   * @constructor
   */
  function Shaker( location, dragBounds, orientation, solute, maxDispensingRate, visible ) {

    var thisShaker = this;
    Movable.call( thisShaker, location, dragBounds );

    thisShaker.orientation = orientation;
    thisShaker.solute = solute;
    thisShaker.maxDispensingRate = maxDispensingRate;

    thisShaker.visible = new Property( visible );
    thisShaker.empty = new Property( false );
    thisShaker.dispensingRate = new Property( 0 );
    thisShaker.previousLocation = location;

    // set the dispensing rate to zero when the shaker becomes empty or invisible
    var observer = function() {
      if ( thisShaker.empty.get() || !thisShaker.visible.get() ) {
        thisShaker.dispensingRate.set( 0 );
      }
    };
    thisShaker.empty.link( observer );
    thisShaker.visible.link( observer );
  }

  inherit( Movable, Shaker, {

    reset: function() {
      Movable.prototype.reset.call( this );
      this.visible.reset();
      this.empty.reset();
      this.dispensingRate.reset();
      this.previousLocation = this.location.get(); // to prevent shaker from dispensing solute when its location is reset
    },

    // Sets the dispensing rate if the shaker is moving.
    step: function() {
      var thisShaker = this;
      if ( thisShaker.visible.get() && !thisShaker.empty.get() ) {
        if ( thisShaker.previousLocation.equals( thisShaker.location.get() ) ) {
          thisShaker.dispensingRate.set( 0 ); // shaker is not moving, don't dispense anything
        }
        else {
          thisShaker.dispensingRate.set( thisShaker.maxDispensingRate ); // max rate seems to work fine
        }
      }
      thisShaker.previousLocation = thisShaker.location.get();
    }
  } );

  return Shaker;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * A particle that comes out of the shaker.
 * The particle falls towards the surface of the solution, may bounce off the wall
 * of the beaker, and disappears when it hits the surface of the solution (or bottom of the beaker,
 * if the beaker is empty.)
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/ShakerParticle',['require','PHET_CORE/inherit','BEERS_LAW_LAB/concentration/model/SoluteParticle','DOT/Vector2'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var SoluteParticle = require( 'BEERS_LAW_LAB/concentration/model/SoluteParticle' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * Constructor
   * @param {Solute} solute
   * @param {Vector2} location in the beaker's coordinate frame
   * @param {Number} orientation in radians
   * @param {Vector2} initialVelocity
   * @param {Vector2} acceleration
   * @constructor
   */
  function ShakerParticle( solute, location, orientation, initialVelocity, acceleration ) {

    SoluteParticle.call( this, solute.particleColor, solute.particleSize, location, orientation );

    this.solute = solute;
    this.velocity = initialVelocity;
    this.acceleration = acceleration;
  }

  inherit( SoluteParticle, ShakerParticle, {

    /**
     *  Propagates the particle to a new location.
     *  @param {Number} deltaSeconds
     *  @param {Beaker} beaker
     */
    step: function( deltaSeconds, beaker ) {
      // mutable calls added to remove the number of new objects we create
      this.velocity = this.acceleration.times( deltaSeconds ).add( this.velocity );
      var newLocation = this.velocity.times( deltaSeconds ).add( this.location.get() );

      /*
       * Did the particle hit the left wall of the beaker? If so, change direction.
       * Note that this is a very simplified model, and only deals with the left wall of the beaker,
       * which is the only wall that the particles can hit in practice.
       */
      var minX = beaker.getLeft() + this.solute.particleSize;
      if ( newLocation.x <= minX ) {
        newLocation.setX( minX );
        this.velocity.setX( Math.abs( this.velocity.x ) );
      }

      this.location.set( newLocation );
    }
  } );

  return ShakerParticle;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Manages the lifetime of shaker particles, from creation when they exit the shaker,
 * to deletion when they are delivered to the solution.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/ShakerParticles',['require','BEERS_LAW_LAB/concentration/model/ShakerParticle','DOT/Vector2'], function( require ) {
  

  // imports
  var ShakerParticle = require( 'BEERS_LAW_LAB/concentration/model/ShakerParticle' );
  var Vector2 = require( 'DOT/Vector2' );

  // Units for speed and acceleration are not meaningful here, adjust these so that it looks good.
  var INITIAL_SPEED = 100;
  var GRAVITATIONAL_ACCELERATION_MAGNITUDE = 150;

  // These offsets determine where a salt particle originates, relative to the shaker's location.
  var MAX_X_OFFSET = 20;
  var MAX_Y_OFFSET = 5;

  /**
   * @param {Shaker} shaker
   * @param {ConcentrationSolution} solution
   * @param {Beaker} beaker
   * @constructor
   */
  function ShakerParticles( shaker, solution, beaker ) {

    var thisParticles = this;

    thisParticles.shaker = shaker;
    thisParticles.solution = solution;
    thisParticles.beaker = beaker;
    thisParticles.particles = []; // ShakerParticle
    thisParticles.changedCallbacks = []; // function(ShakerParticle)

    // when the solute changes, remove all particles
    solution.solute.link( function() {
      thisParticles._removeAllParticles();
    } );

    // remove all particles if the solute amount goes to zero.
    solution.soluteAmount.link( function( amount ) {
      if ( amount === 0 ) {
        thisParticles._removeAllParticles();
      }
    } );
  }

  ShakerParticles.prototype = {

    /**
     * Registers a callback that will be notified when particles move, or when all particles are removed.
     * @param {ShakerParticles~Callback} callback
     */
    registerParticleChangedCallback: function( callback ) {
      this.changedCallbacks.push( callback );
    },

    // Particle animation and delivery to the solution, called when the simulation clock ticks.
    step: function( deltaSeconds ) {

      var beaker = this.beaker;
      var shaker = this.shaker;
      var solution = this.solution;
      var changed = this.particles.length > 0;

      // propagate existing particles
      for ( var i = this.particles.length - 1; i >= 0; i-- ) {

        var particle = this.particles[i];
        particle.step( deltaSeconds, beaker );

        // If the particle hits the solution surface or bottom of the beaker, delete it, and add a corresponding amount of solute to the solution.
        var percentFull = solution.volume.get() / beaker.volume;
        var solutionSurfaceY = beaker.location.y - ( percentFull * beaker.size.height ) - solution.solute.get().particleSize;
        if ( particle.location.get().y > solutionSurfaceY ) {
          this._removeParticle( particle );
          solution.soluteAmount.set( solution.soluteAmount.get() + ( 1 / solution.solute.get().particlesPerMole ) );
        }
      }

      // create new particles
      if ( shaker.dispensingRate.get() > 0 ) {
        var numberOfParticles = Math.round( Math.max( 1, shaker.dispensingRate.get() * solution.solute.get().particlesPerMole * deltaSeconds ) );
        for ( var j = 0; j < numberOfParticles; j++ ) {
          this._addParticle( new ShakerParticle( solution.solute.get(), getRandomLocation( this.shaker.location.get() ), getRandomOrientation(),
            this._getInitialVelocity(), this._getGravitationalAcceleration() ) );
        }
      }

      changed = changed || this.particles.length > 0;

      if ( changed ) {
        this._fireParticlesChanged();
      }
    },

    // Computes an initial velocity for the particle.
    _getInitialVelocity: function() {
      return Vector2.createPolar( INITIAL_SPEED, this.shaker.orientation ); // in the direction the shaker is pointing
    },

    // Gravitational acceleration is in the downward direction.
    _getGravitationalAcceleration: function() {
      return new Vector2( 0, GRAVITATIONAL_ACCELERATION_MAGNITUDE );
    },

    _addParticle: function( particle ) {
      this.particles.push( particle );
    },

    _removeParticle: function( particle ) {
      this.particles.splice( this.particles.indexOf( particle ), 1 );
    },

    _removeAllParticles: function() {
      var particles = this.particles.slice( 0 );
      for ( var i = 0; i < particles.length; i++ ) {
        this._removeParticle( particles[i] );
      }
      this._fireParticlesChanged();
    },

    // Notify that at least one particle was added, removed, or moved
    _fireParticlesChanged: function() {
      var changedCallbacks = this.changedCallbacks.slice( 0 );
      for ( var i = 0; i < changedCallbacks.length; i++ ) {
        changedCallbacks[i]();
      }
    }
  };

  // Gets a random location relative to some origin
  var getRandomLocation = function( origin ) {
    // (Math.floor(Math.random() * (randNumMax - randNumMin + 1)) + randNumMin);
    var xOffset = getRandomInt( -MAX_X_OFFSET, MAX_X_OFFSET ); // positive or negative
    var yOffset = getRandomInt( 0, MAX_Y_OFFSET ); // positive only
    return new Vector2( origin.x + xOffset, origin.y + yOffset );
  };

  // Gets a random number in a range
  var getRandomInt = function( min, max ) {
    return (Math.floor( Math.random() * (max - min + 1) ) + min);
  };

  // Gets a random orientation, in radians.
  var getRandomOrientation = function() {
    return Math.random() * 2 * Math.PI;
  };

  return ShakerParticles;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Color scheme for relating concentration to color.
 * The scheme also defines the concentration range for the solute, where maxConcentration
 * is synonymous with 'saturated'.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/SoluteColorScheme',['require','SCENERY/util/Color'], function( require ) {
  

  // imports
  var interpolateRBGA = require( 'SCENERY/util/Color' ).interpolateRBGA;

  /**
   * @param {Number} minConcentration
   * @param {Color} minColor
   * @param {Number} midConcentration
   * @param {Color} midColor
   * @param {Number} maxConcentration
   * @param {Color} maxColor
   * @constructor
   */
  function SoluteColorScheme( minConcentration, minColor, midConcentration, midColor, maxConcentration, maxColor ) {
    this.minColor = minColor;
    this.midColor = midColor;
    this.maxColor = maxColor;
    this.minConcentration = minConcentration;
    this.midConcentration = midConcentration;
    this.maxConcentration = maxConcentration;
  }

  SoluteColorScheme.prototype = {

    /**
     * Converts a concentration value to a Color, using a linear interpolation of RGB colors.
     * @param {Number} concentration moles (M)
     * @return {Color} color
     */
    concentrationToColor: function( concentration ) {
      if ( concentration >= this.maxConcentration ) {
        return this.maxColor;
      }
      else if ( concentration <= this.minConcentration ) {
        return this.minColor;
      }
      else if ( concentration <= this.midConcentration ) {
        return interpolateRBGA( this.minColor, this.midColor, ( concentration - this.minConcentration ) / ( this.midConcentration - this.minConcentration ) );
      }
      else {
        return interpolateRBGA( this.midColor, this.maxColor, ( concentration - this.midConcentration ) / ( this.maxConcentration - this.midConcentration ) );
      }
    }
  };

  return SoluteColorScheme;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model of a solute, immutable.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/Solute',['require','BEERS_LAW_LAB/common/BLLSymbols','SCENERY/util/Color','BEERS_LAW_LAB/concentration/model/SoluteColorScheme','BEERS_LAW_LAB/common/model/Solvent','BEERS_LAW_LAB/beers-law-lab-strings'], function( require ) {
  

  // imports
  var BLLSymbols = require( 'BEERS_LAW_LAB/common/BLLSymbols' );
  var Color = require( 'SCENERY/util/Color' );
  var SoluteColorScheme = require( 'BEERS_LAW_LAB/concentration/model/SoluteColorScheme' );
  var Solvent = require( 'BEERS_LAW_LAB/common/model/Solvent' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );

  /**
   * Constructor.
   * @param {String} name
   * @param {String} formula
   * @param {Number} stockSolutionConcentration
   * @param {SoluteColorScheme} colorScheme
   * @param {Color|undefined} particleColor
   * @param {Number|undefined} particleSize
   * @param {Number|undefined} particlesPerMole
   * @constructor
   */
  function Solute( name, formula, stockSolutionConcentration, colorScheme, particleColor, particleSize, particlesPerMole ) {
    this.name = name;
    this.formula = formula;
    this.stockSolutionConcentration = stockSolutionConcentration;
    this.colorScheme = colorScheme;
    this.particleColor = particleColor || colorScheme.maxColor;
    this.particleSize = particleSize || 5;
    this.particlesPerMole = particlesPerMole || 200;
  }

  Solute.prototype = {
    // convenience method
    getSaturatedConcentration: function() {
      return this.colorScheme.maxConcentration;
    }
  };

  // Specific solutes ===========================================

  Solute.DRINK_MIX = new Solute(
    strings.drinkMix,
    strings.drinkMix,
    5.5,
    new SoluteColorScheme( 0, new Color( 224, 255, 255 ),
      0.05, new Color( 255, 225, 225 ),
      5.96, new Color( 255, 0, 0 ) ) );

  Solute.COBALT_II_NITRATE = new Solute(
    strings.cobaltIINitrate,
    BLLSymbols.COBALT_II_NITRATE,
    5.0,
    new SoluteColorScheme( 0, Solvent.WATER_COLOR,
      0.05, new Color( 255, 225, 225 ),
      5.64, new Color( 255, 0, 0 ) ) );

  Solute.COBALT_CHLORIDE = new Solute(
    strings.cobaltChloride,
    BLLSymbols.COBALT_CHLORIDE,
    4.0,
    new SoluteColorScheme( 0, Solvent.WATER_COLOR,
      0.05, new Color( 255, 242, 242 ),
      4.33, new Color( 255, 106, 106 ) ) );

  Solute.POTASSIUM_DICHROMATE = new Solute(
    strings.potassiumDichromate,
    BLLSymbols.POTASSIUM_DICHROMATE,
    0.5,
    new SoluteColorScheme( 0, Solvent.WATER_COLOR,
      0.01, new Color( 255, 204, 153 ),
      0.51, new Color( 255, 127, 0 ) ) );

  Solute.POTASSIUM_CHROMATE = new Solute(
    strings.potassiumChromate,
    BLLSymbols.POTASSIUM_CHROMATE,
    3.0,
    new SoluteColorScheme( 0, Solvent.WATER_COLOR,
      0.05, new Color( 255, 255, 153 ),
      3.35, new Color( 255, 255, 0 ) ) );

  Solute.NICKEL_II_CHLORIDE = new Solute(
    strings.nickelIIChloride,
    BLLSymbols.NICKEL_II_CHLORIDE,
    5.0,
    new SoluteColorScheme( 0, Solvent.WATER_COLOR,
      0.2, new Color( 170, 255, 170 ),
      5.21, new Color( 0, 128, 0 ) ) );

  Solute.COPPER_SULFATE = new Solute(
    strings.copperSulfate,
    BLLSymbols.COPPER_SULFATE,
    1.0,
    new SoluteColorScheme( 0, Solvent.WATER_COLOR,
      0.2, new Color( 200, 225, 255 ),
      1.38, new Color( 30, 144, 255 ) ) );

  Solute.POTASSIUM_PERMANGANATE = new Solute(
    strings.potassiumPermanganate,
    BLLSymbols.POTASSIUM_PERMANGANATE,
    0.4,
    new SoluteColorScheme( 0, Solvent.WATER_COLOR,
      0.01, new Color( 255, 0, 255 ),
      0.48, new Color( 80, 0, 120 ) ),
    Color.BLACK );

  return Solute;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model container for the 'Concentration' screen.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/model/ConcentrationModel',['require','BEERS_LAW_LAB/concentration/model/Beaker','DOT/Bounds2','BEERS_LAW_LAB/concentration/model/ConcentrationMeter','BEERS_LAW_LAB/concentration/model/ConcentrationSolution','DOT/Dimension2','BEERS_LAW_LAB/concentration/model/Dropper','BEERS_LAW_LAB/concentration/model/Evaporator','BEERS_LAW_LAB/concentration/model/Faucet','BEERS_LAW_LAB/concentration/model/Precipitate','AXON/Property','DOT/Range','BEERS_LAW_LAB/concentration/model/Shaker','BEERS_LAW_LAB/concentration/model/ShakerParticles','BEERS_LAW_LAB/concentration/model/Solute','DOT/Vector2'], function( require ) {
  

  // imports
  var Beaker = require( 'BEERS_LAW_LAB/concentration/model/Beaker' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var ConcentrationMeter = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationMeter' );
  var ConcentrationSolution = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationSolution' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var Dropper = require( 'BEERS_LAW_LAB/concentration/model/Dropper' );
  var Evaporator = require( 'BEERS_LAW_LAB/concentration/model/Evaporator' );
  var Faucet = require( 'BEERS_LAW_LAB/concentration/model/Faucet' );
  var Precipitate = require( 'BEERS_LAW_LAB/concentration/model/Precipitate' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var Shaker = require( 'BEERS_LAW_LAB/concentration/model/Shaker' );
  var ShakerParticles = require( 'BEERS_LAW_LAB/concentration/model/ShakerParticles' );
  var Solute = require( 'BEERS_LAW_LAB/concentration/model/Solute' );
  var Vector2 = require( 'DOT/Vector2' );

  // constants
  var BEAKER_VOLUME = 1; // L
  var SOLUTION_VOLUME_RANGE = new Range( 0, BEAKER_VOLUME, 0.5 ); // L
  var SOLUTE_AMOUNT = new Range( 0, 6, 0 ); // moles
  var DEFAULT_SOLUTE_AMOUNT = 0; // moles
  var MAX_EVAPORATION_RATE = 0.25; // L/sec
  var MAX_INPUT_FLOW_RATE = 0.25; // L/sec
  var MAX_OUTPUT_FLOW_RATE = MAX_INPUT_FLOW_RATE; // L/sec
  var DROPPER_FLOW_RATE = 0.05; // L/sec
  var SHAKER_MAX_DISPENSING_RATE = 0.2; // mol/sec

  function ConcentrationModel() {

    var thisModel = this;

    // Solutes, in rainbow (ROYGBIV) order.
    thisModel.solutes = [
      Solute.DRINK_MIX,
      Solute.COBALT_II_NITRATE,
      Solute.COBALT_CHLORIDE,
      Solute.POTASSIUM_DICHROMATE,
      Solute.POTASSIUM_CHROMATE,
      Solute.NICKEL_II_CHLORIDE,
      Solute.COPPER_SULFATE,
      Solute.POTASSIUM_PERMANGANATE
    ];

    // model elements
    thisModel.solute = new Property( thisModel.solutes[0] );
    thisModel.solution = new ConcentrationSolution( thisModel.solute, DEFAULT_SOLUTE_AMOUNT, SOLUTION_VOLUME_RANGE.defaultValue );
    thisModel.beaker = new Beaker( new Vector2( 400, 550 ), new Dimension2( 600, 300 ), 1 );
    thisModel.precipitate = new Precipitate( thisModel.solution, thisModel.beaker );
    thisModel.shaker = new Shaker( new Vector2( thisModel.beaker.location.x, 170 ), new Bounds2( 225, 50, 625, 210 ), 0.75 * Math.PI, thisModel.solute, SHAKER_MAX_DISPENSING_RATE, true );
    thisModel.shakerParticles = new ShakerParticles( thisModel.shaker, thisModel.solution, thisModel.beaker );
    thisModel.dropper = new Dropper( new Vector2( thisModel.beaker.location.x, 225 ), new Bounds2( 250, 225, 570, 225 ), thisModel.solute, DROPPER_FLOW_RATE, false );
    thisModel.evaporator = new Evaporator( MAX_EVAPORATION_RATE, thisModel.solution );
    thisModel.solventFaucet = new Faucet( new Vector2( 155, 220 ), -400, 45, MAX_INPUT_FLOW_RATE );
    thisModel.drainFaucet = new Faucet( new Vector2( 800, 630 ), thisModel.beaker.getRight(), 45, MAX_OUTPUT_FLOW_RATE );
    thisModel.concentrationMeter = new ConcentrationMeter( new Vector2( 785, 210 ), new Bounds2( 10, 150, 835, 680 ),
      new Vector2( 750, 370 ), new Bounds2( 30, 150, 966, 680 ) );

    // Things to do when the solute is changed.
    thisModel.solute.link( function( solute ) {
      thisModel.solution.soluteAmount.set( 0 );
    } );

    // Enable faucets and dropper based on amount of solution in the beaker.
    thisModel.solution.volume.link( function( volume ) {
      thisModel.solventFaucet.enabled.set( volume < SOLUTION_VOLUME_RANGE.max );
      thisModel.drainFaucet.enabled.set( volume > SOLUTION_VOLUME_RANGE.min );
      thisModel.dropper.enabled.set( !thisModel.dropper.empty.get() && ( volume < SOLUTION_VOLUME_RANGE.max ) );
    } );

    // Empty shaker and dropper when max solute is reached.
    thisModel.solution.soluteAmount.link( function( soluteAmount ) {
      var containsMaxSolute = ( soluteAmount >= SOLUTE_AMOUNT.max );
      thisModel.shaker.empty.set( containsMaxSolute );
      thisModel.dropper.empty.set( containsMaxSolute );
      thisModel.dropper.enabled.set( !thisModel.dropper.empty.get() && !containsMaxSolute && thisModel.solution.volume.get() < SOLUTION_VOLUME_RANGE.max );
    } );
  }

  ConcentrationModel.prototype = {

    // Resets all model elements
    reset: function() {
      this.solute.reset();
      this.solution.reset();
      this.shaker.reset();
      this.dropper.reset();
      this.evaporator.reset();
      this.solventFaucet.reset();
      this.drainFaucet.reset();
      this.concentrationMeter.reset();
    },

    /*
     * Moves time forward by the specified amount.
     * @param deltaSeconds clock time change, in seconds.
     */
    step: function( deltaSeconds ) {
      this._addSolventFromInputFaucet( deltaSeconds );
      this._drainSolutionFromOutputFaucet( deltaSeconds );
      this._addStockSolutionFromDropper( deltaSeconds );
      this._evaporateSolvent( deltaSeconds );
      this._propagateShakerParticles( deltaSeconds );
      this._createShakerParticles();
    },

    // Add solvent from the input faucet
    _addSolventFromInputFaucet: function( deltaSeconds ) {
      this._addSolvent( this.solventFaucet.flowRate.get() * deltaSeconds );
    },

    // Drain solution from the output faucet
    _drainSolutionFromOutputFaucet: function( deltaSeconds ) {
      var drainVolume = this.drainFaucet.flowRate.get() * deltaSeconds;
      if ( drainVolume > 0 ) {
        var concentration = this.solution.concentration.get(); // get concentration before changing volume
        var volumeRemoved = this._removeSolvent( drainVolume );
        this._removeSolute( concentration * volumeRemoved );
      }
    },

    // Add stock solution from dropper
    _addStockSolutionFromDropper: function( deltaSeconds ) {
      var dropperVolume = this.dropper.flowRate.get() * deltaSeconds;
      if ( dropperVolume > 0 ) {
        var volumeAdded = this._addSolvent( dropperVolume );
        this._addSolute( this.solution.solute.get().stockSolutionConcentration * volumeAdded );
      }
    },

    // Evaporate solvent
    _evaporateSolvent: function( deltaSeconds ) {
      this._removeSolvent( this.evaporator.evaporationRate.get() * deltaSeconds );
    },

    // Propagate solid solute that came out of the shaker
    _propagateShakerParticles: function( deltaSeconds ) {
      this.shakerParticles.step( deltaSeconds );
    },

    // Create new solute particles when the shaker is shaken.
    _createShakerParticles: function() {
      this.shaker.step();
    },

    // Adds solvent to the solution. Returns the amount actually added.
    _addSolvent: function( deltaVolume ) {
      if ( deltaVolume > 0 ) {
        var volumeBefore = this.solution.volume.get();
        this.solution.volume.set( Math.min( SOLUTION_VOLUME_RANGE.max, this.solution.volume.get() + deltaVolume ) );
        return this.solution.volume.get() - volumeBefore;
      }
      else {
        return 0;
      }
    },

    // Removes solvent from the solution. Returns the amount actually removed.
    _removeSolvent: function( deltaVolume ) {
      if ( deltaVolume > 0 ) {
        var volumeBefore = this.solution.volume.get();
        this.solution.volume.set( Math.max( SOLUTION_VOLUME_RANGE.min, this.solution.volume.get() - deltaVolume ) );
        return volumeBefore - this.solution.volume.get();
      }
      else {
        return 0;
      }
    },

    // Adds solvent to the solution. Returns the amount actually added.
    _addSolute: function( deltaAmount ) {
      if ( deltaAmount > 0 ) {
        var amountBefore = this.solution.soluteAmount.get();
        this.solution.soluteAmount.set( Math.min( SOLUTE_AMOUNT.max, this.solution.soluteAmount.get() + deltaAmount ) );
        return this.solution.soluteAmount.get() - amountBefore;
      }
      else {
        return 0;
      }
    },

    // Removes solvent from the solution. Returns the amount actually removed.
    _removeSolute: function( deltaAmount ) {
      if ( deltaAmount > 0 ) {
        var amountBefore = this.solution.soluteAmount.get();
        this.solution.soluteAmount.set( Math.max( SOLUTE_AMOUNT.min, this.solution.soluteAmount.get() - deltaAmount ) );
        return amountBefore - this.solution.soluteAmount.get();
      }
      else {
        return 0;
      }
    }
  };

  return ConcentrationModel;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Collection of utility function related to Strings.
 */
define('PHETCOMMON/util/StringUtils',[], function() {
  

  function StringUtils() {
  }

  /**
   * http://mobzish.blogspot.com/2008/10/simple-messageformat-for-javascript.html
   * Similar to Java's MessageFormat, supports simple substitution, simple substitution only.
   * The full MessageFormat specification allows conditional formatting, for example to support pluralisation.
   * <p>
   * Eg, StringUtils.format( '{0} + {1}', 2, 3 ) -> '2 + 3'
   *
   * @param {String} pattern pattern string, with N placeholders, where N is an integer
   * @return {String}
   */
  StringUtils.format = function( pattern ) {
    var args = arguments;
    return pattern.replace( /{(\d)}/g, function( r, n ) { return args[+n + 1];} );
  };

  return StringUtils;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Visual representation of a beaker that is filled to the top with a solution.
 * Ticks on the left edge of the beaker. Origin is at the bottom center.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/BeakerNode',['require','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY_PHET/PhetFont','KITE/Shape','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Shape = require( 'KITE/Shape' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );

  // constants
  var MAX_VOLUME = 1;
  var RIM_OFFSET = 20;
  var MINOR_TICK_SPACING = 0.1; // L
  var MINOR_TICKS_PER_MAJOR_TICK = 5;
  var MAJOR_TICK_LENGTH = 30;
  var MINOR_TICK_LENGTH = 15;
  var TICK_LABEL_X_SPACING = 8;
  var MAJOR_TICK_LABELS = new Array( '\u00bd', '1' ); // 1/2, 1

  /**
   * Constructor
   * @param {Beaker} beaker
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function BeakerNode( beaker, mvt ) {

    var thisNode = this;
    Node.call( thisNode, { pickable: false } );

    // outline of the beaker, starting from upper left
    var width = mvt.modelToViewDeltaX( beaker.size.width );
    var height = mvt.modelToViewDeltaY( beaker.size.height );
    var outlineShape = new Shape()
      .moveTo( -(width / 2 ) - RIM_OFFSET, -height - RIM_OFFSET )
      .lineTo( -(width / 2), -height )
      .lineTo( -(width / 2), 0 )
      .lineTo( width / 2, 0 )
      .lineTo( width / 2, -height )
      .lineTo( (width / 2) + RIM_OFFSET, -height - RIM_OFFSET );
    thisNode.addChild( new Path( outlineShape,
      {
        stroke: 'black',
        lineWidth: 3,
        lineCap: 'round',
        lineJoin: 'round'
      } ) );

    // horizontal tick marks, left edge, from bottom up
    var ticksParent = new Node();
    thisNode.addChild( ticksParent );
    var numberOfTicks = Math.round( MAX_VOLUME / MINOR_TICK_SPACING );
    var deltaY = height / numberOfTicks;
    for ( var i = 1; i <= numberOfTicks; i++ ) {

      // tick
      var isMajorTick = ( i % MINOR_TICKS_PER_MAJOR_TICK === 0 );
      var y = -( i * deltaY );
      var leftX = -width / 2;
      var rightX = leftX + ( isMajorTick ? MAJOR_TICK_LENGTH : MINOR_TICK_LENGTH );
      var tickShape = new Shape().moveTo( leftX, y ).lineTo( rightX, y );
      var tickPath = new Path( tickShape,
        {
          stroke: 'black',
          lineWidth: 2,
          lineCap: 'butt',
          lineJoin: 'bevel'
        } );
      ticksParent.addChild( tickPath );

      // major tick label
      if ( isMajorTick ) {
        var labelIndex = ( i / MINOR_TICKS_PER_MAJOR_TICK ) - 1;
        if ( labelIndex < MAJOR_TICK_LABELS.length ) {
          var label = StringUtils.format( strings.pattern_0value_1units, MAJOR_TICK_LABELS[labelIndex], strings.units_liters );
          ticksParent.addChild( new Text( label, {
            font: new PhetFont( 24 ),
            fill: 'black',
            x: rightX + TICK_LABEL_X_SPACING,
            centerY: tickPath.centerY
          } ) );
        }
      }
    }

    var location = mvt.modelToViewPosition( beaker.location );
    thisNode.x = location.x;
    thisNode.y = location.y;
  }

  inherit( Node, BeakerNode );

  return BeakerNode;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Used to create meter bodies (eg, the Concentration meter in Beer's Law Lab).
 * The body has 3 parts: left, right, center.
 * The left and right nodes can be thought of as 'book ends', with the center node stretched to fill the space in the middle.
 * <p>
 * NOTE: The center image should be something that can be stretched, versus tiled. A more general solution would
 * (and in fact, did) use tiling via scenery.Pattern.  But that solution has vertical alignment problems. For more
 * details on the problem, and whether it's been fixed, see scenery#132.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/MeterBodyNode',['require','ASSERT/assert','SCENERY/nodes/Image','PHET_CORE/inherit','SCENERY/nodes/Node'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );

  /**
   * @param {Number} totalWidth
   * @param leftImage
   * @param centerImage
   * @param rightImage
   * @param options
   * @constructor
   */
  function MeterBody( totalWidth, leftImage, centerImage, rightImage, options ) {

    assert && assert( leftImage.height === centerImage.height && centerImage.height === rightImage.height ); // all images are the same height
    assert && assert( ( leftImage.width + rightImage.width ) <= totalWidth );  // center may be unused

    options = _.extend( {
      overlap: 1  // how much the center overlaps with the left and right
    }, options );

    Node.call( this );

    // left
    var leftNode = new Image( leftImage );
    this.addChild( leftNode );

    // right
    var rightNode = new Image( rightImage );
    rightNode.right = totalWidth;
    this.addChild( rightNode );

    // center, stretched to fit
    var centerNode = new Image( centerImage );
    var centerWidth = totalWidth - leftNode.width - rightNode.width + ( 2 * options.overlap );
    centerNode.setScaleMagnitude( centerWidth / centerImage.width, 1 );
    centerNode.left = leftNode.right - options.overlap;
    this.addChild( centerNode );

    this.mutate( options );
  }

  inherit( Node, MeterBody );

  return MeterBody;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * A drag handler for something that is movable and constrained to some (optional) bounds.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/common/view/MovableDragHandler',['require','PHET_CORE/inherit','SCENERY/input/SimpleDragHandler','DOT/Vector2'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * @param {Movable} movable
   * @param {ModelViewTransform2} mvt
   * @param {*} options
   * @constructor
   */
  function MovableDragHandler( movable, mvt, options ) {

    options = _.extend( {
      endDrag: function() { /* do nothing */ }  // use this to do things at the end of dragging, like 'snapping'
    }, options );

    var startOffset; // where the drag started, relative to the movable's origin, in parent view coordinates

    SimpleDragHandler.call( this, {

      allowTouchSnag: true,

      // note where the drag started
      start: function( event ) {
        var location = mvt.modelToViewPosition( movable.location.get() );
        startOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).minus( location );
      },

      // change the location, adjust for starting offset, constrain to drag bounds
      drag: function( event ) {
        var parentPoint = event.currentTarget.globalToParentPoint( event.pointer.point ).minus( startOffset );
        var location = mvt.viewToModelPosition( parentPoint );
        var constrainedLocation = constrainBounds( location, movable.dragBounds );
        movable.location.set( constrainedLocation );
      },

      end: function( event ) {
         options.endDrag();
      },

      translate: function( options ) {
        // override default behavior, do nothing
      }
    } );
  }

  inherit( SimpleDragHandler, MovableDragHandler );

  /**
   * Constrains a point to some bounds.
   * @param {Vector2} point
   * @param {Bounds2} bounds
   */
  var constrainBounds = function( point, bounds ) {
    if ( _.isUndefined( bounds ) || bounds.containsCoordinates( point.x, point.y ) ) {
      return point;
    }
    else {
      var xConstrained = Math.max( Math.min( point.x, bounds.maxX ), bounds.x );
      var yConstrained = Math.max( Math.min( point.y, bounds.maxY ), bounds.y );
      return new Vector2( xConstrained, yConstrained );
    }
  };

  return MovableDragHandler;
} );

define("image!BEERS_LAW_LAB/../images/concentration-meter-body-left.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAB3CAYAAAD2F+tJAAACCWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS4xLjIiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkFkb2JlIEltYWdlUmVhZHk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Co5SDuoAAAAJcEhZcwAACxMAAAsTAQCanBgAAALwSURBVGje7dpfSFNRHAfw7737c+/W3e7+OjdbeTXNMISkzILMhIiCIB98rRahvUi9mFCI+OaDD2EF/YGoiHwJ8iEIKmq9VURE//DNBfkwIRgNS4fzdM9JhwQ+yM7ood9vHM7DOPfD75zB9jv7KYwxrBVT6bdrv7mOUFYjP3N59uTSfXy88xSFzHe4oEIqknk/xa51D8CTmUNQ9cKsCsEbCUBRFTnIXO4HO28dRTTnQAA6Es11CLdY0CN+echY91mWnXyHMDwIGwEkO1tgbk3AEzWhOMrfMiU7/Y0N2lnUwA8fNIQMExv3bbeRGnjjQahOR/nIxPBl9nLkrp2F13654Fd1xJotBLcl4bOq4NDd5SOjx/rZ18nXMO2z8MApIEPzIlgXhxr3IVafLB8Z6jzBZtNf7K1y24jLppxiaPb4pTGE6hMwDKN8JJv+bJ+IvgpwCETXdeSxgPn5eUIIIYQQQgghhBBCCCGEEEIIIYQQQgghhJD/D9GrA0j0tMFoiMtHnIaOTakO1PS0Q1VVMaQjrbf64GuMw+12i+FyueQim1P7YZ3ugtfrFYOv1zRNLrL38SAC1RH4fD5xK85nqdu1YUsM7ff6EQwGYZqmmIvFIvhfJ9IQc0ct2q73IhqNCoBnsbKOEEIIIYQQQgghhBBC/iGy+0YfIpFI5RBe/HS9GEYsFkM4HBalQ6FQwOLiotzSofXqKTQd2SMQngkPqb/qeRgN1eh8MCCy4eUDX1+RwjTS0YRdo8cRt5KiEJJeaZUq33gAjWcOofbwTvhjocrX8cFWi24kCCFkPQhvtOTdgxzgkNue+b3IAorA0lK5yEk2k/5QQvjD+eAdtirK70sVyMTwFfZo5Ka9XVopA6f9eIdA5ITy6uEzNtZ9TjRb6iILZwlRZCH8S6XXOsjymWxpu5zLWfzZMEmtvJ/Sb9jQgdTyp8pRmUx4PL89ycZTFwTgKp2JIhfhwTMaT13EbGZGYh5/ISsx/X6K2SDmcnkpyG+3UR3Jil+AOgAAAABJRU5ErkJggg==';
return img;});

define("image!BEERS_LAW_LAB/../images/concentration-meter-body-center.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAB3CAYAAAD2F+tJAAAACXBIWXMAAAsTAAALEwEAmpwYAAABAklEQVRo3u3YoU7DAACE4XseBFmATCxBQEimeYDJTcJQyDExNXDLJua3LEvI1ARihCYkbWZA0b5BK5qqVrU5XgFzqLvk158//LwfqQ5jXFMdtqd3VIfP+yXV4Wv6SnX4ftlRHY6jFdXhoz+jOmzaD1SHCW6ozogRI0aMGDFixIgRI0aMGDFixIgRI0akyGH/xiAIpCEMQ0ZRJA1xHDNJEmnI85xFUUhDVVUsy1Ia1IARI0aMGDFixIgRI0bUSNM0VAf+w1DXNdUhTVNmWSYN87M+F+cDab49jBj5A/KMLtVhii7V4QlXVIdHXFIdhuhQHQa4oDr00KI63OKE6rAezajuF8piXcN/K5n0AAAAAElFTkSuQmCC';
return img;});

define("image!BEERS_LAW_LAB/../images/concentration-meter-body-right.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAB3CAYAAAD2F+tJAAACCWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS4xLjIiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkFkb2JlIEltYWdlUmVhZHk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Co5SDuoAAAAJcEhZcwAACxMAAAsTAQCanBgAAAMgSURBVGje7dpNSFRRFAfwgxtdtGhZOxe1MLVS+qA0UzPLHMjQvjBLo8nURgslg7LJLNJ0MD/6siyj0NTKSGgWGpI4wzQDjUOICNKgBNoq00ZR0H/vXrOPRRC8K7g4D/4w3AfvN+fet5gzHOrvckFFtIv+FSqmWKjIRYrGlcBktJnvwvd1/C+UWtfmQm9aQkxoWJGGKr9EXKYYFAYa4HX3/4LIlncfetNjqkPn4Qo0B+egkhJg1qDs5VH4/vWbhKi3/CX0xl36HPb8elgNJahbth/XaScKKBLlSbnziMfyCnrjLnsB25kHeJNQLJFSiscF2o50CsMX72eQy9wIvXEUNqDjwA20hJpQ42fANYqTyCnaiCZzDeidsRZ60xJbiGdB2bjjvw/ltEu+becpClm0CaVJJlDzprPQk/o1RpT7J6KWDLDQbllFkfY6F1AEMrVKiqKPgcSinlgCDKgLSEY1JcoqrmprFyUSiZO0gRFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYWTxkfbqJ2gy3YBlRcriIXa7HT09Peju7pZY2TKDeqSvrw+9vb1wOp0Ssz5sVY+Mjo5iaGgIAwMDcLvdsNlseJpRohaZmpqCz+eDwAYHByXUZe1QiwhgcnIS09PTEhIVORwO3FyVqhYRmZ2dxdjYGLxerzyf2rATjDDCCCOMMMIII4wwspQQl8ulHhE/tsUlPg8PD0vkf5uh/0Lm5uawcI2Pj8tKrLca1bYOC4DoU0Tr4PF4ULX6iFpkZmYGExMTGBkZwQe7E/eiTqtv50Qr99HpRnvxfVhWpixO93s73Mj/SDDCyFJAKrQbuuKXIGcfq2iPREpoh0TyKeI3Im6oSBnFyyouUYycgcyjLThO4RqSDhJTsSoiHi6mOAtpG87SVjlomUrr0GSuBYlFFTmnRWyRqCCLNiJDqyKFQuBo6wSd0VS9EQ820WbkaMn8CRyiUBgD4+anbMXhqIhRiziDo7QeBzUgidbgY9f7eSRNW1QRsf/i2ydTMPZSEN4+avs9+bxPE1VFPFxs0UIFvxBx+iryuvIxPv0xt/1nfgCsb1ZYUV138wAAAABJRU5ErkJggg==';
return img;});

define("image!BEERS_LAW_LAB/../images/concentration-meter-probe.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Concentration meter, with probe.
 * <p/>
 * The probe registers the concentration of all possible fluids that it may contact, including:
 * <ul>
 * <li>solution in the beaker
 * <li>output of the solvent faucet
 * <li>output of the drain faucet
 * <li>output of the dropper
 * </ul>
 * <p/>
 * Rather than trying to model the shapes of all of these fluids, we handle 'probe is in fluid'
 * herein via intersection of node shapes.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/ConcentrationMeterNode',['require','SCENERY/nodes/Image','PHET_CORE/inherit','SCENERY_PHET/MeterBodyNode','BEERS_LAW_LAB/common/view/MovableDragHandler','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY_PHET/PhetFont','KITE/Shape','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text','DOT/Util','DOT/Vector2','image!BEERS_LAW_LAB/../images/concentration-meter-body-left.png','image!BEERS_LAW_LAB/../images/concentration-meter-body-center.png','image!BEERS_LAW_LAB/../images/concentration-meter-body-right.png','image!BEERS_LAW_LAB/../images/concentration-meter-probe.png'], function( require ) {
  

  // imports
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var MeterBodyNode = require( 'SCENERY_PHET/MeterBodyNode' );
  var MovableDragHandler = require( 'BEERS_LAW_LAB/common/view/MovableDragHandler' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Shape = require( 'KITE/Shape' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var Vector2 = require( 'DOT/Vector2' );

  // images
  var bodyLeftImage = require( 'image!BEERS_LAW_LAB/../images/concentration-meter-body-left.png' );
  var bodyCenterImage = require( 'image!BEERS_LAW_LAB/../images/concentration-meter-body-center.png' );
  var bodyRightImage = require( 'image!BEERS_LAW_LAB/../images/concentration-meter-body-right.png' );
  var probeImage = require( 'image!BEERS_LAW_LAB/../images/concentration-meter-probe.png' );

  // constants
  var BODY_IS_DRAGGABLE = true;
  var VALUE_DECIMALS = 3;
  var NO_VALUE = '-';
  var TITLE_TOP = 12; // specific to bodyCenterImage
  var TEXT_X_MARGIN = 25;  // specific to bodyCenterImage
  var VALUE_X_MARGIN = 30; // specific to bodyCenterImage
  var VALUE_CENTER_Y = 84; // specific to bodyCenterImage

  /**
   * Meter body, origin at upper left.
   * Note that while the body is a Movable, we have currently decided not to allow it to be moved,
   * so it has no drag handler
   * @param {ConcentrationMeter} meter
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function BodyNode( meter, mvt ) {

    var thisNode = this;
    Node.call( thisNode, {
      cursor: 'pointer'
    } );

    // text nodes
    var titleNode = new Text( strings.concentration,
      { font: new PhetFont( 18 ), fill: 'white' } );
    var unitsNode = new Text( StringUtils.format( strings.pattern_parentheses_0text, strings.units_molesPerLiter ),
      { font: new PhetFont( 18 ), fill: 'white' } );
    var valueNode = new Text( ( 1 ).toFixed( VALUE_DECIMALS ),
      { font: new PhetFont( 24 ), fill: 'black' } );

    // create a background that fits the text
    var maxTextWidth = Math.max( titleNode.width, unitsNode.width );
    var bodyWidth = ( 2 * TEXT_X_MARGIN ) + maxTextWidth;
    var backgroundNode = new MeterBodyNode( bodyWidth, bodyLeftImage, bodyCenterImage, bodyRightImage );

    // rendering order
    thisNode.addChild( backgroundNode );
    thisNode.addChild( titleNode );
    thisNode.addChild( unitsNode );
    thisNode.addChild( valueNode );

    // layout
    titleNode.centerX = backgroundNode.centerX;
    titleNode.top = TITLE_TOP;
    unitsNode.centerX = backgroundNode.centerX;
    unitsNode.top = titleNode.bottom + 5;
    valueNode.right = backgroundNode.right - VALUE_X_MARGIN; // right justified
    valueNode.centerY = VALUE_CENTER_Y;

    if ( BODY_IS_DRAGGABLE ) {
      thisNode.addInputListener( new MovableDragHandler( meter.body, mvt ) );
    }

    // body location
    meter.body.location.link( function( location ) {
      thisNode.translation = mvt.modelToViewPosition( location );
    } );

    // displayed value
    meter.value.link( function( value ) {
      if ( isNaN( value ) ) {
        valueNode.setText( NO_VALUE );
        valueNode.centerX = backgroundNode.centerX; // center justified
      }
      else {
        valueNode.setText( value.toFixed( VALUE_DECIMALS ) );
        valueNode.right = backgroundNode.right - VALUE_X_MARGIN; // right justified
      }
    } );
  }

  inherit( Node, BodyNode );

  /**
   * Meter probe, origin at center of crosshairs.
   * @param {Movable} probe
   * @param {ModelViewTransform2} mvt
   * @param {Node} solutionNode
   * @param {Node} stockSolutionNode
   * @param {Node} solventFluidNode
   * @param {Node} drainFluidNode
   * @constructor
   */
  function ProbeNode( probe, mvt, solutionNode, stockSolutionNode, solventFluidNode, drainFluidNode ) {

    var thisNode = this;
    Node.call( thisNode, {
      cursor: 'pointer'
    } );

    var imageNode = new Image( probeImage );
    thisNode.addChild( imageNode );
    var radius = imageNode.height / 2; // assumes that image height defines the radius
    imageNode.x = -radius;
    imageNode.y = -radius;

    // probe location
    probe.location.link( function( location ) {
      thisNode.translation = mvt.modelToViewPosition( location );
    } );

    // touch area
    var dx = 0.25 * imageNode.width;
    var dy = 0.25 * imageNode.height;
    thisNode.mouseArea = Shape.rectangle( imageNode.x - dx, imageNode.y - dy, imageNode.width + dx + dx, imageNode.height + dy + dy );

    // drag handler
    thisNode.addInputListener( new MovableDragHandler( probe, mvt ) );

    var isInNode = function( node ) {
      return node.getBounds().containsPoint( probe.location.get() );
    };

    thisNode.isInSolution = function() {
      return isInNode( solutionNode );
    };

    thisNode.isInSolvent = function() {
      return isInNode( solventFluidNode );
    };

    thisNode.isInDrainFluid = function() {
      return isInNode( drainFluidNode );
    };

    thisNode.isInStockSolution = function() {
      return isInNode( stockSolutionNode );
    };
  }

  inherit( Node, ProbeNode );

  /**
   * Wire that connects the body and probe.
   * @param {Movable} body
   * @param {Movable} probe
   * @param {Node} bodyNode
   * @param {Node} probeNode
   * @constructor
   */
  function WireNode( body, probe, bodyNode, probeNode ) {

    var thisNode = this;
    Path.call( thisNode, new Shape(), {
      stroke: 'gray',
      lineWidth: 8,
      lineCap: 'square',
      lineJoin: 'round',
      pickable: false // no need to drag the wire, and we don't want to do cubic-curve intersection here, or have it get in the way
    } );

    var updateCurve = function() {

      // Connect bottom-center of body to right-center of probe.
      var bodyConnectionPoint = new Vector2( bodyNode.centerX, bodyNode.bottom - 10 );
      var probeConnectionPoint = new Vector2( probeNode.right, probeNode.centerY );

      // control points
      // The y coordinate of the body's control point varies with the x distance between the body and probe.
      var c1Offset = new Vector2( 0, Util.linear( 0, 800, 0, 200, bodyNode.centerX - probeNode.left ) ); // x distance -> y coordinate
      var c2Offset = new Vector2( 50, 0 );
      var c1 = new Vector2( bodyConnectionPoint.x + c1Offset.x, bodyConnectionPoint.y + c1Offset.y );
      var c2 = new Vector2( probeConnectionPoint.x + c2Offset.x, probeConnectionPoint.y + c2Offset.y );

      thisNode.shape = new Shape()
        .moveTo( bodyConnectionPoint.x, bodyConnectionPoint.y )
        .cubicCurveTo( c1.x, c1.y, c2.x, c2.y, probeConnectionPoint.x, probeConnectionPoint.y );
    };
    body.location.link( updateCurve );
    probe.location.link( updateCurve );
  }

  inherit( Path, WireNode );

  /**
   * @param {ConcentrationMeter} meter
   * @param {ConcentrationSolution} solution
   * @param {Dropper} dropper
   * @param {Node} solutionNode
   * @param {Node} stockSolutionNode
   * @param {Node} solventFluidNode
   * @param {Node} drainFluidNode
   * @param {ModelViewTransform2} mvt
   * @param strings
   * @constructor
   */
  function ConcentrationMeterNode( meter, solution, dropper, solutionNode, stockSolutionNode, solventFluidNode, drainFluidNode, mvt, strings ) {

    var thisNode = this;
    Node.call( thisNode );

    var bodyNode = new BodyNode( meter, mvt, strings );
    var probeNode = new ProbeNode( meter.probe, mvt, solutionNode, stockSolutionNode, solventFluidNode, drainFluidNode );
    var wireNode = new WireNode( meter.body, meter.probe, bodyNode, probeNode );

    // rendering order
    thisNode.addChild( wireNode );
    thisNode.addChild( bodyNode );
    thisNode.addChild( probeNode );

    var updateValue = function() {
      if ( probeNode.isInSolution() ) {
        meter.value.set( solution.concentration.get() );
      }
      else if ( probeNode.isInSolvent() ) {
        meter.value.set( 0 );
      }
      else if ( probeNode.isInDrainFluid() ) {
        meter.value.set( solution.concentration.get() );
      }
      else if ( probeNode.isInStockSolution() ) {
        meter.value.set( dropper.solute.get().stockSolutionConcentration );
      }
      else {
        meter.value.set( NaN );
      }
    };
    meter.probe.location.link( updateValue );
    solution.solute.link( updateValue );
    solution.concentration.link( updateValue );
    solutionNode.addEventListener( 'bounds', updateValue );
    stockSolutionNode.addEventListener( 'bounds', updateValue );
    solventFluidNode.addEventListener( 'bounds', updateValue );
    drainFluidNode.addEventListener( 'bounds', updateValue );
  }

  inherit( Node, ConcentrationMeterNode );

  return ConcentrationMeterNode;
} );

// Copyright 2002-2013, University of Colorado

/**
 * A circular node that inherits Path, and allows for optimized drawing,
 * and improved parameter handling.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/Circle',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/nodes/Path','KITE/Shape'], function( require ) {
  

  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );

  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );

  scenery.Circle = function Circle( radius, options ) {
    if ( typeof radius === 'object' ) {
      // allow new Circle( { radius: ... } )
      // the mutators will call invalidateCircle() and properly set the shape
      options = radius;
    } else {
      this._radius = radius;

      // ensure we have a parameter object
      options = options || {};

    }
    // fallback for non-canvas or non-svg rendering, and for proper bounds computation

    Path.call( this,Shape.circle( 0, 0, radius ), options );
  };
  var Circle = scenery.Circle;

  inherit( Path, Circle, {
    invalidateCircle: function() {
      // setShape should invalidate the path and ensure a redraw
      this.setShape( Shape.circle( 0, 0, this._radius ) );
    },

    // create a circle instead of a path, hopefully it is faster in implementations
    createSVGFragment: function( svg, defs, group ) {
      return document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' );
    },

    // optimized for the circle element instead of path
    updateSVGFragment: function( circle ) {
      circle.setAttribute( 'r', this._radius );

      circle.setAttribute( 'style', this.getSVGFillStyle() + this.getSVGStrokeStyle() );
    },

    getBasicConstructor: function( propLines ) {
      return 'new scenery.Circle( ' + this._radius + ', {' + propLines + '} )';
    },

    getRadius: function() {
      return this._radius;
    },

    setRadius: function( radius ) {
      if ( this._radius !== radius ) {
        this._radius = radius;
        this.invalidateCircle();
      }
      return this;
    },

    computeShapeBounds: function() {
      // optimization, where we know our computed bounds will be just expanded by half the lineWidth if we are stroked (don't have to compute the stroke shape)
      return this._stroke ? this._shape.bounds.dilated( this._lineDrawingStyles.lineWidth / 2 ) : this._shape.bounds;
    },

    // accelerated hit detection
    containsPointSelf: function( point ) {
      return point.x * point.x + point.y * point.y < this._radius * this._radius;
    },

    get radius() { return this.getRadius(); },
    set radius( value ) { return this.setRadius( value ); }
  } );

  // not adding mutators for now
  Circle.prototype._mutatorKeys = [ 'radius' ].concat( Path.prototype._mutatorKeys );

  return Circle;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * A toggle button changes it's on/off state when pressed.
 * Can optionally be configured as a 'momentary' button, which is on while pressed, off when released.
 * Origin is at the upper-left of the bounding rectangle.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/common/view/ToggleButton',['require','SCENERY/input/ButtonListener','SCENERY/nodes/Image','PHET_CORE/inherit','SCENERY/nodes/Node'], function( require ) {
  

  // imports
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );

  /**
   * @param unpressedImage
   * @param pressedImage
   * @param disabledImage
   * @param {Property<Boolean>} on
   * @param {Property<Boolean>} enabled
   * @param {*} options
   * @constructor
   */
  function ToggleButton( unpressedImage, pressedImage, disabledImage, on, enabled, options ) {

    options = _.extend( { onWhilePressed: false }, options );

    var thisButton = this;
    Node.call( this );

    var imageNode = new Image( unpressedImage );
    thisButton.addChild( imageNode );

    on.link( function( on ) {
      if ( enabled.get() ) {
        imageNode.setImage( on ? pressedImage : unpressedImage );
      }
    } );

    enabled.link( function( enabled ) {
      if ( enabled ) {
        imageNode.setImage( on.get() ? pressedImage : unpressedImage );
        thisButton.cursor = 'pointer';
      }
      else {
        imageNode.setImage( disabledImage );
        thisButton.cursor = 'default';
      }
    } );

    if ( options.onWhilePressed ) {
      // momentary button, on while pressed, off when released
      thisButton.addInputListener( new ButtonListener( {
        down: function( event ) {
          on.set( enabled.get() );
        },
        up: function( event ) {
          on.set( false );
        },
        over: function( event ) {
          on.set( false );
        }
      } ) );
    }
    else {
      // toggle button, changes its state when fired
      thisButton.addInputListener( new ButtonListener( {
        fire: function() {
          on.set( !on.get() && enabled.get() );
        }
      } ) );
    }
  }

  inherit( Node, ToggleButton );

  return ToggleButton;

} );

define("image!BEERS_LAW_LAB/../images/dropper_foreground.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

define("image!BEERS_LAW_LAB/../images/dropper_background.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

define("image!BEERS_LAW_LAB/../images/red_button_pressed.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

define("image!BEERS_LAW_LAB/../images/red_button_unpressed.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

define("image!BEERS_LAW_LAB/../images/red_button_disabled.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIMAAAB/CAYAAADMzXEEAAAABGdBTUEAANbY1E9YMgAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAC8jSURBVHja7H0JkFzVkW11dVVv6la3pNbSklqSxSYBMkhIg0FiMVgIsFnsCRBeBo/hMwQYf0xgYhx8GxzYwzDwCYPxEliWx2A83xibxQaGXWwaLCSZ1RIghBZAa6s3tXrvqp/ZZKpTSea9tzUxMTYxFfHiVb169eq9e8/NPLncvJnMR/dVAluWNvnKie9xK6NzStVv8oFrZz+KDVb6EXymPHVqkZ6vArYCbKOo46th66PPWfquikAyGrZK+j5Px/FadXSslI4V6PoMmOJHZfR8FJ6hhDomS52fow7rEe/xeA1svQSKbgLGHjqHAYBbG4FlD52bod+V0LV20X4EbB30mwJd83/A8N/w4hFfSe87aI8d3E8juII6qIZ+g+Aop04eSXspUfrp3AH6rpY6vpaus5vO7afrtNDnPiE5sgIUfX/1YNi0adMHX5aUZIrFdAmI52ez2UxPT0+mra1t8FhlZWWmvLw8097enunv7880NjZm3n333Uwul9t7bfxdoVD40LX4NXv27BKhqyuooztIrJfT6C3S89TQ+04a8SMuvvjimWPGjKmC/6ypqKjo6e7uroD3fXA/ZbDPwz2NaGpqKnR2djbDPe394z179nQ3Nze3LF26dANJhgECIEuiHYJnlND3PXRuB/0/gqr4+OOPDwzqE3hmfLaBgYEP9HRpaaa3tzfT19eXgXvL7N69e7C98JzW1tbBdho1atTe8/kF95uB+/2gUbJDFAbbET/z3nrhd5dccsmHjudCncs3ngIIfR53rrwp3OM58iZlpztAyJE4LqGRNpI6ZDy976JTUUJUn3HGGQ3nnXfe8WVlZZOgUSugsSvGjRs3F4GADY+Nint9b/yZn5mfHzqq/XOf+9zr8F0/dFg3AKTpt7/97X3Lli3rAICjFGoWKmmA+EUngbRI35UvXLgQv+967LHH+q023stMRVvpgSLvS59vfbaeDz97IMnFOlYDwQKHPKYfjFEaA5z+zzlz5pQJMY8XqFd6fcS8efPGLliwYOrRRx99TENDw/yqqqp6uA6O9mrdQHgPuEkgyL0FRHwPkm3kpEmTjuXrIJhmzJhxxje/+c2+rq6u9ueee24JjODWtWvXdjzyyCNrCZQI0ia69/EkKfB49ymnnDKojuDcotWRVluF2tCTACFQeK+c10GpakGeH5MiIVAoVVBL7xEQE2hfILVQfc0118yfNWvWoTDi59TW1s7UDaYbQgPCtBUDDS6/QzDBVpPP5xEooz/72c/+E34H4r1l8eLFTwAuNl9xxRWPwqlTSI1kiWswt0EJ0XrqqadmSYL0P/TQQwX93/zZa89Q5+rO19JmWGCIqQDrGH+WI0t3jnUzeP6RRx7JFkEtidcBUg3IDUYcd9xxdWPHjm08//zzT4f9XOiEiajrLTXEHS470BOP8jwNGgvAIX0MoBwFgD4H2+CZZ545BzhT0y9/+ctb7rnnnh3Ch9FLbT5KACTz6U9/uuv+++/vta5r8SmrbVOec78kg6cqQiPfUit8A6yL5UOxyAUgSIdPBb0f9AFMmzat+qqrrvo7OOfsmpqaSboBdOfoRrM6Tt+DNfr17yzQyHM1BwJgTMHt6quvvvPss89+AkjohrvuumvZihUr3iGw99Dp3cR5quA8lH57ADxFa0RjW1ntb3V0CLQhYORCunw4qkBLBt24bGko1VAqHDuo58ei2AQuMOqyyy47e/r06X8LIJjmjVANBA8QVuNaoEDmHpMQljrS0omPoUoBdfYptKIOO+ywz6xfv/6PP/3pT+9auXJlC/GKWuJBvezvOOeccxAoPaw6JJG0CH3oHqz2CUkI17T01ECMM7Bp2dLSMvgeTUvc0EzCG5o4cWJmwoQJ7Bji/Ti8d2i4ugsuuOB0IIRfBjI4zhrhKaNfg8CyWGLWUojfpJxj3St+BrO2sHz58n8FU/NxsCxaSDI0kSnaRX2CxwugZgrITerq6vaxdHAfMy1D94vfg7mdBobNmze7HMGyHCwwSD8DbggOFHVz587NEQBQX46hEVK6ZMmSC4Cln1RdXX2w7jxvJHudrkeTB16vUy0T2RO1GqQWcOV7vvb27du3gdr43bXXXns/ObF2EdlsEZ8HzWmwPAb07xkMoY4PSQELDLmYipBcwBphlklp3QyphAw5hcaQVBgJN3XAmWeeeVFjY+Nxlsi39lL16M631JGlN/VnzXksVRASzSE1ZIEaLKEJp59++lenTJly+PPPP//g0qVLXyCTeUC4v1FSFMHyKAG10c/Paz1j6L9SyGPUz6BHUOhzSNQCEWROkCWJgJ9zt95667nHHnvsBSAKazw1oBuZ36M+1p2Pe/YjaHDqz/L5JMH1TLxUUHikTXcS3g9ylCOOOOKEgw8++ITDDz/8nh//+Mf3rFu3roJM0jraEBTbweIoPPzwwwXuG4tDpZqfnnoLuqM9h1JIhUjOACohSyZigVg0Oo7KgSQdDBLh6vr6+jkh0Sofiq8tAYAdbx3TI0iL6xAQ5GdUa/q84XILj+NIQPJ/QZvtuPHGG6956qmnXiEJgYN1G2ztZHmg+ugFKVEECyXT0dER5E0hoCSrCY8welaEHnl4AwAEzhWoINaMd1R/5ZVXHn7eeeddD9JgjOcg0sjGa3Mnc4fLjSUGjzatPjTXsToe98j6+Thfg7/TZnKKs8ojudJU5A1fY8aMGfeNb3zju2edddZTl19++Y/J0mggabqd1MhOBEqKD2c4KmK/AlWy0y1wYOdMnjyZHUgV7EUE6yB/xx13XAIq4+zy8vIRsYAKi3vuZGTVsuOx0/EzHpcAkWqE75E7GINBPBq58+X32EFSckiQWC7sGBG1CGaIo7Ekwv9bu3btchg4N4C0aCYH1V4gILkES6MHJUNKZ1uDLNmakGoiJi00SIAQlZLjaALd+EQwFSu/+93v/hOYlAtCnjN5nCWBlAAIgLKysr3gwM8SJLoBuHNxzw3N77mjORrIew0EySMkyZQNrCOKKdLCsmDwuRigCFwAxMrVq1c/DVziSThlK7mv8Yet1LaoPjK33357MSVaud9qIiYJNDDw/dSpU9mtPJ5EWykQnzFf//rXrxs7duw8a5RYFgIDgUc/7nnD8K6UCvolASD3evTrTpTA0OrE87BKKSYBEnNhW0DBa7Cjiq916KGHzoPBNW/8+PE1YIL+P+qvErIyUIWMACB0pKiLFJBE1USIM0hAABA404hNx+rvfe97CxctWvR/oNOqY2YZi3UtCaQEQCCwJAhJAYsIyo7m95ZKsELw2voI+SZC0UWPR8jv5P+gdMB7RX/C8uXLbwdA/I78Dx3k0u6iEDp2VL+UEDEgWJIhG8tnkFvI80ivKgJD7pprrjkNgPBtBIIVSJKNwmKeQYAdjuoANwQAbsA5BpM/ZAgaGwotF04O4T0e4z0eQ9GL5+Iej1vg0RaFBJRlZocGiWfmSdPYinFIX4lUlei0mz9//sXf+c53PkvtO47MztGkjsu5L2M+lRBgs6kqQt6kfHhUDSDKqujm0HQceeGFFx51xhlnXA2dWKWjh1ZwR6oHJoQoDbDzEQi4x+NMArmTudP5Pe+503Gv3+M15HEEA3+v+YQGgqUGpDMuVV97kUXLm4vtwG59AMQl11133ReEDyJPfggk6zkY7Vnvv3Rkdthg0Fk/zuioIpcyqoZRoOfGgen4v6HzKi0njgaBlAqsEhgArBZYZEoJwO/lcQ8A/J7PYSkR6nypPrxO0yZriBPoa3imofSTSFDgHiXlJz7xiX/41re+tUi0/WgilWMos8p13EW9lvuTxyCkAucjIlmsmTFjxvhbbrnleiCLR1kuYe1JlL4DBgKDAT9zriCO9u7ubrPzpZrQ3/OeQcGdz5aFBLjkGpJTSJVkkUhPaqR6/2IZV7Ld2Jo6/vjjLwOz8wsEhDoi7OiUwnzPEZ7vJpTcE7UmEnIgS8iMLJ81a1blzTfffN24ceOODIV8ZT6fNB0ZCCgR2CkjO03qcEkI9Tm6I70RHiPF2lyMZYGl6OpYWpp8Ru3ZxcGBwMa2QkCAyviH9vb2ziVLljxKXsoJZGp2ASA62eSMxWmGZVrqZEzgBzIrCclMDaiGj91www3/yECIhZxl/ICBwHtJ3hgUFuu3vIg64GR1CF5Pi/NYsoyXwxDSzzHzLsQvJAdhycmJvPxcI0eOzJx44oln/OIXv1gGIKklxxTnRlT98Ic/3IPgsZ7fC+kncQbjx2Xk9MB9/Ve/+tXTJk2atCA1DYvPYxBIpxETOrlnES95gTyH30szUqsCPdp1x1ipciHRrtWevs5wIon6fM1H5P9LYgnq+AAY/VdD+42ggVlGg7T2sssuyw03DyObQh6d32Fq2sSvfe1rM2bPnn2uZTpaFoMVVOIUeovxS7NQgoItCw8AslM9Xenp71AD6usO9zqWOgllaOvfSkKJgJg4ceLRwNOuHjVqVD1FhqvI1MRssVLYspIbhUzkbIpZqV7lhMKGSy+9tPHzn//89ZiiHkRcIALJAJIdypsEhAQF76Xa0K7jmLhOzWZKDfjEgJQSGpeBtdB9yMGEgGhsbJz/pS996WRKG6wU+9wHXVmSBrYURwq5mrNkPYwlU7L3k5/85LkAhNGetWDxBS0VNBBYKvBeqwrea4Yfa3yP8WsnU0gSxDyKqWQxxj1iE4u4DXlDh9yCBQvOnDdv3kSSDDzVr+KD7itGXeouGOSPxA+LZMagVKj8yU9+cv5BBx30aa0vPeeSlWPgef4kMPR7bSGE1ID1sgJWUrrooJblRk9xLnlgic13SJDMJmnF7HGQDl8n3jCKuAMO2mpQ5WVJKiwk1lQjV1GiSh5Mm4Yjjjji7+EmSmPJqBYQJFvW9r2UDrJTrKSUFMeOVjdaFen/1oCQ96IJ6P5KFQ0qi0x66YSeqsXtYx/72PSLLrroJOrXkbThAM6lgNiE38aNG/e56enTp9eQg2MUgGDktdde+204djJcuCSUpqZvnFmwF0zxQsWpAJDncA6CF3GNqcfQlEErqSc018JK6PXMTG0ey0m6MtLK7aUJ9q5duwbArLx4xYoVW+A03NpIbWwREn74pqV4+AoCTtmiRYuOAQR+ygKSJSF0VrV8CLlpJ5EOIFm6VzN7makUS8pxVOE+7ndtVmt/hrw3j79oHhXKVbTAzllW+j75/tg852vU1dWVApf7DPVPFVkYpeSuHr47WqJ+2rRplZR7h3qn8tRTT71YivlYqBRvmp0lcqqY1SHSwyi/t2YzSZLKZqkU/zqpV3euJmQaEF743sp10NLLy9vwkmktqZiSCqj5GQe1QHqfNmvWrPFk+ZVSQKt8v8CgHrxIuqf+uuuuWwT27BSN9NhcSi8nQDak1N+WTtbX1r4H2YmeiI/5UrxwtQSSBo5MltGcJjTfw+MMnicyBA4d/q6srCz/e3gR2R9LIe8+GtD5YasJTG+HLU8Xq8Z5j3Pnzj0NRnmp9tR509AsUiRFq+YC8nvP12+JaAvEenRr8e9xCcfj6kZytfqzIp5W4MsKLVug1JIglgHNamPChAlHLly4cBoN5m7mfJmh6jLp1gTpHC6KlQOz5W/wD0IoDd2sfmgLCB4D18CTo9ELqln5CPIcLTnk8RDfCPEMbb56ktAaRN65If+GBIgEMX6uqKgYOX/+/OPArDyRJHsVqftcxinsFiKQ7LQYDE8fe+yxf6cnu8aIUcg3r4EQq50gGyfFvWqRP9nxHieQqiA0ScibXSalgA57h7ZQ+pyXS6KlrE4SOuiggz5z1113vUTqIk+gyHqWZIgzsDtzFBbHGDdu3MH6JmMzoCw9Khsq5nqVYhZzE3QENQSC2OjV6sJSK/I6Vv0J/XyhCb7WOaF78iRszMSW/wMqo+Lcc89ddOGFF87MDE1dYMdhMSmEfcABB3CeAv5rFrjCF9h88ZI4rOxfDQqdNpfiP5CSwLIAUvaxAJyVPzAcEHlmnzaLOXzuTVDSvxlucTXtqi4vLy/5+Mc/vqitrQ0tibWkKrpJKuRIbQQlA2c4DxIO0DsNZWVlVZ7tnBKcskKzKZk3Uop48z9TnUi6wT1CGRrdISJq8Q8NeD3DKzVxJvZcOiFGvurq6qasWrVqHUmCLEmGrCUILDAUMkOFMAsgZo6pr68/UD9QqCaAlxeQ+tA6qynW+J5u9ywDq/OtXE+rco1FVkP/o6f1eSBN9axa1/CKoeBnGMglBx544FEkDeozQ6WESnT/W2DI08n5kSNH1tbU1DTocLPlQPGsDM/tHJpUIomV7izLLPQKi2jyllLGcDgASh3B/L+hORKpkjI1jU1yj5kzZ545Z86ciSIFoZ44RCFFTYygOMT4GTNmnLM/SRueL8JSMVYtRm0+WgzcMhP1iA3xgdCotEzVkMqwpJJWS9rfEGrTVNCGkmN4D9KhdPbs2fOEhdiTGcpfLQ2BYRT/YPHixafICaxWZ3q8wXOjehlDFmmyzDlLUniWgNdhluQJmY6eAyskDWKmbyhTyvsPDSLdJ56qxO/BKJgjos+VIiWh6IEhlxmamDFq6tSpJ1oVUixvWsjtGvNU8ibDxpbu98S91/iW1AhZCDHbP1Qo1QKm9dmL7KbmY4TMa6sPuP9A3U+AVyVJgnIyEHKemigR0ckq9mHrB7E6MSUHMFaEygKAFx+weIHX0d5MMAmw0KgP8Qcr8mmxegtEMVURm8VlmaYc3WT1IV3Z1dXV48EynK944YDEgAZDGYmQkiuvvHIBEMjGkJjzfAqh8z3JoWsUhLx8HrlMKVzqkciYQ8nT26HOTwGRF7+x2s6rYMcp9bLIiJY8yBumTZs2F6RDrQBCpSSRsgd5sQ6MbuUmTpw4KZ/Pl+sb1JNHtSt6OClo0hnD6sGqI22NdAsIVmzCA0ooNiH/I2S+eqCNFUaz7j01M9tz2OkUAOt6kydPPhYAMYGj0AQGXoTlQ2BANTEaI5Tjxo0brxvECymHTMmYL0HPXLK8jYlzPk397fEMDyQhiRGqmmtZLR6gvcSaFIecVRYx5AOS7Q2qAtu7oNRDP5NI+Ssu1Vs6a9asAxsaGk6LFQX1yvGm+NUtieKFly0Hj0f0LMngdaZX19Jj5aFOsa5jSZZYLc2UGpU6kcbyYXj8TgQhuwkEZZZkGKxdjCfARXLSitDTxPUfpTiSNIAsczKUcRQjgZZ9HwtohYBueSW968aCWCEXtnUspmY9S4L7zKtMh6/DDz/8k8Abqqi/2e8weFkZzuwiXVKGcxVCZMVKj99fT5klJkPOHo8reGHpkKi2TNSQ4yfGZVKilCkWTWjqXYhjyP7wCodi8VXgg4OVdYgzdJL1uE9sm2s554Ev1HieP4vdhiqzhHzr+liKGRdi9DGWb5G3FLPS6jQrX8K7j5jJ7HWoJojW1EHrWmxiaglMuQ79wBs4aFVG3mY0NYtZ4WwaQSfkzzrrrPNjXjZrPQedzuVFOkPmYiwXwOIIoZB0aBSnZDXFwOg5tTzHU6giv+dNtCRiLDDnDSya0p8lTcBh7MFUBf7nXvJX931wP9lqbb540iHV7xBzmsQ6zitkbo1OK8HFI6YpaW/eNUNWgXWN1GluocintcaXnh5guarFvgQ21gIoGcYSIPJZYUmUk805SCY9D5huAJ0lbRFETx1oEml1rmWOeVLL63AviBTT/SG1E/JcehaEJS1SIqmh6KYnUby2z+fzddOnT58rAlWdxB96pGTg1dZ6ZfAiNqNJOjhS0r9lsqhO8Y6ZcqG0dis9LeRttPwXnqvbc2SFcipiIns4gakUn4hMEQxln2eGch8HBFdEjVAhe4xL2pdIEEg3qTRddAfFVm3xLJMUfe11WIwAhvwWHgeJOZ20urKcT7G5GCGAenEKq52tijWxOSyoJoAz1GaG1szidTh7c8oVXRDI2cdDqPMPPPLm3XSMhIZs8JQJMTGLwRLZVs6k5zsIqY/h5FumPEeo7bDNuDaFvqbOELMmK8Pv8/X19UdmhhaD53XA97Emyim+XRmyX2P6LBRCjtnJFhH03L4hT15KnmRK5lMs2TXGbUKezFhAKlb2N+SosiS2/B4siqLwRPKWz4poZUdGLOwdSjbRtqvnCg7Z0yEgeDGJkFnlSarUGEZIbYQ4hEUEvewqy5PpOZyGwxekZLACi0YbFzNDCbKc6ZTL0sEukgyDK8rLEjkhSWDlIFgd5CWN6N9rky/FeeQRTI/4xbKfvWuGOIo18j2TNyT9NBC8kgSWGWmtpaFnoCkrj6fdsUuhOytMDF4fKWuN6tQiWV5Ku+4Ia4k+yzkTctikpKyFLInQsZDVEjNtQylz1rEQqfZ4BJcgCOVSWN5KAMZAR0fHeyQVWACgaVnMCfK4h8Cwz9R2WbfR87zFElx0wQmpF626hMNJXE0Rz1bOg5csa3n8vJB6zIRNVXFeKoBe2DSW9eS5sRWA+pqbm98S7oMBMhxy3HvdxBtyVgKLJYZCDhHLtxBLgvGIoSXiPX+/pbut6w7HF5EKQE8KhMAYWrIxRNitMknSwrCkg1x2iaRCiXBFI00Y4KtyIQckkUVZTcWqqeiRyJSCmtoWDk2OtTrYilCGEl5CI9yLlKZ4AzWvic2QinErHRH2UuOs9HgJEKvijWFy7pUGJBlKZQgb/3E3IQbzGbpisQnZqR45HE4dRo+Rp1gpoREZq+WUMhMqxeUdqvOQEs4ODR5dGlkDM1RgTP+2r6+vbcOGDS+TZChmhiZY56U7mv+luG7dusetCR/eDcYIWggclsiP+fRDEcSQK9hKpg35SkJJrp6fIpTKlqqGQouZWK5ma+F5vTIfvohwFqFv+0gy9BFFaJdqIk9f4r7v0UcfXaXXYvBMnNAcwNhkktRQc2oneBIi5sH0MqpSJJGnhlIzuEMvHpBsNXjpbbqcoi64LvnCnj17ukE6FGjw95GaQGuij8FQEKZld11dXTnXZbaiZKnVSFLMvdBkVs9CCLmRLekU6ghv7kUoPyGktjw/ifUcVqKxRyi1B9JT457K4PdgSaxvaWnZQ5RggPwM3TLTaSAztCJaX2trazcgqZ2LaHnL+oWIm3duqFSOldruBYBiiSYxb10MGKFzPRXkeRpDMRY92i3epa05XZzUKpUo1+2U63Q1NTWt2b59O/NDlhB7JIFkVdGEwNi5c2frrl271mob1cvHD1VEjSV3hhxUKVVRYmZdyGoI5SR48zFjYA7pf09C8WiXoLEWP7EcT1YbeyvyISi2bt36NhwvChBwCYaiBEMnS4qNGzf2btu2bY2UCjpmHvI9hLKghjPBJuRbCHGKkHqIuaVDbu+Qe9pSB1pdeLUvvXqR1nGrYp6V7yiLsbOEB/XwDgz0JgJCvyCQGakmMpmhBUUGbc+enh4kGkWriLc2V+So8KyQlNL/IYsklmUcSoLxRLtFdv8zE228gFeKi1rPG5G8IFQwVMcrrNV4eA984e3333+/Q/DEfvIx9GgwFOjzbpIO74BY6dIqwgpixSaAhGzgmAcylIySEleIVXfxVIRFbGNzQFO5RoqpKj2Kmn95OY+a28k+Q4Ogu7t7u1ARHUIyfGgSDaKkjfRI+0MPPfRmR0fHdrnkj/XnIV9DTEVYE25CySQhiRFi+V5He0Gj1NlRw5E8VpzAciTFln+03MxWFFiDp7OzcwAkw1Yx8PtEIlNBg4HtzsE5l/DjIuiY9RKhuqK5x3g9sWvVgA5NHws5k0ImpEdCPVPWG6UeEEOexeF4SrVktNbTkEXWpXWnib2OZvLG1+3q6tr28ssvr6a/6yGOyCTyQ3Mt+TOetB2B8cADD/xGq4ZYHD00Kr3opsUjvMpsITIX08+hmdheipuVdxCa9+ldx5txbU1dtKwMnY+qVa/sIznY8DhaEe3t7cAdd3aJDCf2L5hT8jMkGdrZ+bR58+Y2UBU79IIdnijywqspxSqsSJw3g8mLYaS6dz1AebkLoZxKj9x6ZmqomJlXHlj7ETxfjl5mQUqIV1999d+ESuilQR+sA1kiTu4Hm7TrrbfeelCLHL0mhAWIENGzvrcm44Y6PVT9zTMxhwOOUBArlZuE/B/cjvyd59yzzEu5Yo7lZMLPXFEXOR+o+53vvPPOZhGH6iAgtGYCNZ2YNzTRD3rfe++9zaBveq2V42JOpdCiYrHwthfSDqW0pXS2pzpSJ7KkZEGH/CN69POAYnUQC0bptpP+BL0KDw/iLVu2PAsqolOYk92ZxNrRDAhEUDfwhlcAWW9axbf4GN+IzLWTN+etOh9ylngryqQklHgV4FJyC0IEODavI+Qat5Yg0GtkyXbQZF2SRbmCn2xrClHvc87u3btb1q9f/4zqbwRES0bVj/bKrPQRywSjorPnxRdf/A1IhyIHr2T+necR80KvIZ0eS8cPLQLiuY1T4igpUcxQdlNqcCzVS6tX6PWcSfp6UmozcQTOh/kLW1XeCqc4lkTB8KMf/aifJAN6JUvuu+++P8OF+72V3ywCZC016K3YIhstVgbIm5UdSqS1yJ03qcYT/bG8BC8Erlfi06PeawsrMcVqT4trIAhw4OIegPAoSPYeMcibaV9IkgzUIT2Eoi646MC77777MF7cAgSjMaTjPAbsLQjmZUN5aWMpdZ5i3syYu9nLX/DIsmU5cD6o57jzrDEriUWnw8vloPF9c3PzjmefffYxAYQOsiKaUznD4NI2FNLexRwCuMMDYKtu6Onp2WdxML5ZaW3oZfYsyaGZsWd+6gf2OENKOeBQbQRvAm/Ide0ltFijWl5HT4/TwJDrbmkJrJNV5G/keuLYT2BB/AbUfJ/yIeUzQ1Mp42DACwlbFAEx8NprrzVt3LjxBekF02Ftiyha6kFPI7PYs9aPMe6RMi8y5MpOyU/0/CYe2C2VaEkET0JZQNCLr+o9S4Vdu3ZtePnll5eLy3WTKbmd+jaTBAa84G233VYQ0gFjFpnvf//7v96xY8crUl1owqNR75XX98oIehNjPWeMlzASmxicSiQ9d3Io2SaUKugRStnZ2rLQakLyBA5CsUTo7u5G13Nm06ZNT2zbtm2PcD8jCDouvvji3ksuuaSQDAZetJwu0kagGIx7r1y58j4mKIxCaepIlWGZUPIBnWlfZrq3lWDjrVmVkomUmpoXmuqnRbu8P0vsW/dtmZWyDfRe/o80IbFPWIW3tbW1PPnkkw+Lx9hN/ddz++23u6PBXJYIV10XDYNAwKVzsfB0/v33338PuMO7+Xy+UfrU8SbkfAj+rP3p1tT+1IQXvVCqtwxSrOR/aL5lKE0tNLvJWmVHj36vkIYHeB2IstYM12t1g+XQ8tJLL/1fGKScKtVBvA/DDB0XXXRRUS8NHZQMs2bNGtxWrVolhxC6M3v/+Mc/7gB9tAxRyOaLns0jrQ3PN6+JoTdx1Pq9fK8bargp/CkqREs4bkyL3WtHkuW2l22k20znJehryHOkVGbJsGXLluXQR6+J20cwbBMRafdlggEYKKZUD27PPPNMgVRFK7mp+37wgx/cC6bmMyyW2MFhJdDq9am1T93jAN5EkJAJp3M0U8roWZu1nLF8Dul9TcnhsKwhb41OixfIoJPkCaymse1x2759+5rVq1ffrXJUBkgitAJfGD4YsJPlRhftJRfmHgBL/9133/0j0E3bGAiMVMsZZY2WkLkpnTNWh0pAacYuR5nVkcPdNEn2VraVulyvb2lJF+1ZlK5kbzBJULA0YMmM/fTqq6/+HCy+NuFt3EpSoXXJkiUl0SWPPKcTL7KN2xNPPMFJEC0kJQbWr1/fvWbNmn9DO1ZKBun80MQyNrpD+XyWhJD6M1Raz1NPIfViVVSz7kWOYG0l6fuyrA1uH+nql5JTm45SErNkxkSkt95663dA7t8Sf8P1mgZzHpErhDKnXAK5c+fODyWw/upXvyp+8Ytf7Cdmiv6HmjvvvPPp8ePHz5w6deppeB7eYFlZ2aA1IksOI7CkCck61ypX403Rl3o9ZVEvSTY9vwHehzeJR+daeKatJpVeARJL6lmpahpU2tPLQGC+gKYkmJCrH3zwwTvF7e0hwoiDtxuA0B+LFrtgaG1t9RqBHRfYggfAVv7II4/cvXjx4mPhfW15efnezmfJwjpOdpBOgWOwIIgszmAtu6yJn5V9HfqN56FMLXUc6lSLz3hJq5bqkfxAkkWdRkAZTN0vvPDCT6kEMKuHXhq07QCE3tRlj4JqQr7HDWzUAoEBJcNO/NPXXnutGVD5LQDQLhRbrMO0frPyINSEUNP68HSn5fX0xH6MN8j79RJurMXOQ/EE/VsvwCdVqSbg/FlyBEkYd+/e3fnKK6/cuG7dum0ii4kdhVsuvPDCTuu5hgUGb2KHONZEwQ4UQwUwZTbde++93+7o6Ohl8SVBIR8Wv2eUe42kYxoW4bRyIHQmFv6Ptl4sZ44Eok7vC+lvfd+eY8gyOS1iag0ezb8YCGjpwUC8bfny5aukloXtPeqb3qVLl5ZYRVKG5XRC09Jble7mm2/G+g39V111FaKPFzgb96c//em9WbNm/WzmzJkXgdjOS+RXVFR8yCmlStEF3bWhMnha5XgLrkoHl1Yl3lQ2zXF0ylooF9F6Fis10PLCSkkpJQIPMgQC8oSnn376PxQQthHBbwb10GetARJcsjrUyLr0r5IO7JlsItZauOOOOx5Zu3btz+FmiywB8HwkOVp1WI4UfcwS6VbCiuW8CblyQ6n62pfgqS4tMS2z0ctctrKbtGSUHIHbEtuRgPDiAw888C/CiVQktd3MkebQ7G1vcJVaB0855ZToqiZwTvHxxx8v0DVQwgyumQg6bF1DQ0N7bW3tEaVYfdKpzaQXJ5N7+Z2XDKM7KbYqnVcz0uMeXtEs7STiz9a1LP+Kjt9ozsCqjQHAYEA+hgGoLVu2vPjrX//6e3CsXwChi3wKqLb70HrQyUJ6ysJLL72UJhmiRIP+4KabbuIM2y0kogYrwIDJ+fDrr7/+MyA4BdZxMrKGD6Z1onS4WIEsKwQeqnVoeTktkmet+ubNP7DcyZ64t3iKzESSElJKA24LBgCTcgbCfffdd6NSDQyENjIn+2MZY8OSDKeeeuo+hcT1CieyqPjChQsHQEL0EbAwcQLty9JXX3317dGjR7eAhDgSLJFSy+a3RD6XCfTS27T3L7S8sfw/qU6kVOH7smaWh3IPrZoJXiRSklENIOmkY0tBSgVWDVu3bl1xzz33/HP/EIEpklX3Pg3INpQIRx111D6SVa51Kd8Dx0sDw6JFiz602IVcRVWvqkqA4Ir0RQJEbs2aNevr6+ubARBz4LxSq96iV+pPZxZLqeFNsdP35WVkW3kVIQeQJUWsYt36PrVVohNTpLktJQJvKA2QzG/atOnff//73982sC/T3UPSAFXDDjAj+/TMKy/lb9hg8JJG9bK6/OAgTQqPPfbYHlIVdWytACDeAQnRBIA4FH5XHpoboMvaefMbU+ZMeGQzFJjylkvQATbrtzoPVHayVhnSn4DvWRVI0xslAjqUwHy8Edr1/sLQTRVIIiAQNiIYJEfwwumaj1lgyIUmwITMEOf4AHm+8CankJSouPfee5/CKf5HH330FxsbG4/G36Lb2gKWtos50cbLWbDuxVv9bTiFQrwy/zFvpCVppJdSW1KsIjjYhyBAcAAQdgDJuwU67XVjTgtaDO9iW6MJqZ/VMidTXkHJoB84VJCC+EMG1MUAIbedOMRgAibovDbgEf8xadKkuqqqqgOtrGFN0CSaLRIpyxCH5kp4kcaYtJDWlDQzdbKqFWzynEnSgpAeRVYLCITt27evWrZs2Q1vvvnmRjXQummgbaVBN8AcIaQaLEvJsiZMMGCn6jI0lsixil+efPLJmeOPP74AD8PTuKqYVKL//M9//vNqUBs9QCpHl5eX11qdLkmrNZPLMxe9yGRMTViA8v7XSjqxLBFpLekYg/Qd4HnIC9iZtGHDhrufe+65f4XB06okQgdJhPfJgujH/ARtMmq1Kden4DbDz8mcAf0MsoOs2UySmVqJqCeddFLhqaee4hzKUpqQk4XflQCPWNvS0rJq1KhRBwIg6uH8Ei+PQd6DV9cotuqNVYDMmwlm5V1qkFhAkO+tcLTsfFQFfB4nsII0WL1q1apbnn/++WUivZ3VQgs5lVA1dCJHmD17dtJUQ4sLeZzBNS11lrKc+GEVnZAWAv/pggULiiJTKpcZWs6gtKmpqRMe/km47nq0NoAblOn4ghTBmtBa3j+d4GJFE72Yh5XaFpoHogNLMqIoQcEqQqoDNhnx865du1rWrVv38z/84Q9Ld+zY0aSkAQcENxEgOsFqKEi3+nAyvGW/JYMB1YS2FrzCFNoE1e7XE044oQCAYPf1bgLECHrYLIjF9+H8V+Dc7srKykPgP0qsa0s9bU3v95xBOjikJY8X2bSCVB4I9GQiWYhdBu1YEuCGqgFUwfLVq1fftnLlytWqC3qJc+0gdz++70EgyI7VIX9PYlsqxAKDmQF68803m4Egz4MlLQ/ZUHJq+PXXX19CZBLXzhyH+Q+0lfB9zJgxY8Ixxxzzv8aOHXsokMxqDp3jQ+uwOoNT+tv1qrupSyOHLJNQ5hNPlbNK7chnl/MfUQW0trauAlX5WzAb31ZJquxa7qA4AwKh7ctf/nKBn92LMehVh73+4f3SpUvTwHDTTTdlUhMiNEBYDA7Cu7d3b4OgKYkECa7NazAjKA4hz2Ulu8ZHjhyZnzlz5nR4zZ88efLZIC325lPgxgCQJqgEhwWMFDPSi3aGeIeVoCLVi7QiUBKAanxm8+bNT61YsUJT+aLIM91F7n2Uor0oDfAaDIaQW1mblKFzlixZ8p8DgxUK1X/GupHT4BgUGMZGMOAov+GGG1hVIAgmYgpd5oNi1jl5TyApGubOnbt4zJgxR40YMWIwk0qCgRuIV8qxAOJJBe+4nsWlQ+OhGdEMfHYg4R5UQidIgtfeeOONe0ASrDPS1YvCFN9GYBhcq5olQgwMIantgd6SDLmUEW/lAOyP1ODXFVdcUYSH6wDQ9WaG6hBOI1Dwqu0l0IBbYbvl0EMPnXLYYYedUl1dPbm+vn4OgoKXS2IVosFgFczyRGtIEugsZx2wkjxDhuwBAGtwPgvwglfg9YbVN6QSeOobz2stAAj6GAShPkkFQKq0zKUCQSeRePmG1s3qG+fvr7zyyl7gJxzkepukw2TyTTAoMqBfN8P2M1QhAIwZICmmTZky5WxQPVWgRkbg9VANSaJr6dXQYu3ekkB6roP0JEq/ARZQha2lubkZC3WvevPNN18CMHQ5Xtp+ii3g9xtIGg7WV/rKV75StNbz2p+X1fGhgZxLQZ43azr0BzGXMBMvlBKwH7jtttu6hZcNQdEo8iTQaVXS3t7eR7OFXoPOf+SQQw5pAFXyKQBEXT6fnwhgORj2H1qo3WtQrxyfFql6riSrApAAzcAFNnR0dAAV2PwsqIH1ob7JDM1Zbc0MVVBBidCHKsFbdCQkwUIg0FI9ltySG0bcwS3IZaWQeVnGrN/16Lv00ksHoCMHbr311h4SndhgY4lo1orw+GA/Qmf0QeNvhu3neKChoaFm6tSp08EKGVNXVzcZpMeJcL1KTLCB+8rhK0SuQllP8F/dsO/r6uraiZ0PKuCN3bt3bwMpgGUKtg1YeXNDAGDHUQ91/iY6PlhiEUBQDKXfxUZ6SC3rjLVYGmEudrGUYJVWHykJtpwer1+XX345pmx1g6TIkut1F0mKkWSSsjczL5NzQDfvRv2M70FqlDY2Nj4GYMjV1tbWwTYBpMd0AMkMuNcKS3fywp8YHYQRioXUsZ7V26AGurEMQVtbWyvWtwIS3IHRxEBfSMugKLKVm0gKIAD6rTayiotrCRtqu9g8jxiQSjJ/+a8sbRX0GcPjEwgcJfRdmfRX/De85PoNeVIBvLVnhkrz9v0lN/RfAxg0MNgszZAKqSI1MkKYpllx7n/Fi+V5D3Uy17/iIpstmaHqKLx25F/8668NDPreyzNDC7tz2l2NAEiZAAXHRUrUNaREKdDGGVu8ECi/7yIg8NpOfRR32SUkRK+41l9dg35UXgyKjOj0crEvF9ZJnt73Emg4dlJOnVxKn/eWTiYAtNJ/tNPvuRD3QCZS++B/wPCX8XxZwSuyYsSWC5HP1c/66Pge+m1OBI64s/s+qo31/wUYAPzJnw3ZJkyDAAAAAElFTkSuQmCC';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Dropper that contains a solute in solution form.
 * Origin is at the center of the hole where solution comes out of the dropper (bottom center).
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/DropperNode',['require','SCENERY/nodes/Circle','BEERS_LAW_LAB/concentration/model/ConcentrationSolution','SCENERY/nodes/HTMLText','SCENERY/nodes/Image','PHET_CORE/inherit','BEERS_LAW_LAB/common/view/MovableDragHandler','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY_PHET/PhetFont','KITE/Shape','BEERS_LAW_LAB/common/view/ToggleButton','image!BEERS_LAW_LAB/../images/dropper_foreground.png','image!BEERS_LAW_LAB/../images/dropper_background.png','image!BEERS_LAW_LAB/../images/red_button_pressed.png','image!BEERS_LAW_LAB/../images/red_button_unpressed.png','image!BEERS_LAW_LAB/../images/red_button_disabled.png'], function( require ) {
  

  // imports
  var Circle = require( 'SCENERY/nodes/Circle' );
  var ConcentrationSolution = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationSolution' );
  var HTMLText = require( 'SCENERY/nodes/HTMLText' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var MovableDragHandler = require( 'BEERS_LAW_LAB/common/view/MovableDragHandler' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Shape = require( 'KITE/Shape' );
  var ToggleButton = require( 'BEERS_LAW_LAB/common/view/ToggleButton' );

  // images
  var dropperForegroundImage = require( 'image!BEERS_LAW_LAB/../images/dropper_foreground.png' );
  var dropperBackgroundImage = require( 'image!BEERS_LAW_LAB/../images/dropper_background.png' );
  var buttonPressedImage = require( 'image!BEERS_LAW_LAB/../images/red_button_pressed.png' );
  var buttonUnpressedImage = require( 'image!BEERS_LAW_LAB/../images/red_button_unpressed.png' );
  var buttonDisabledImage = require( 'image!BEERS_LAW_LAB/../images/red_button_disabled.png' );

  // constants
  var DEBUG_ORIGIN = false;
  var BUTTON_Y_OFFSET = 13; // y-offset of button location in dropper image file
  var LABEL_Y_OFFSET = 130; // y-offset of the label's center in dropper image file

  // constants specific to the image file
  var TIP_WIDTH = 15;
  var TIP_HEIGHT = 5;
  var GLASS_WIDTH = 46;
  var GLASS_HEIGHT = 150;
  var GLASS_Y_OFFSET = TIP_HEIGHT + 14;

  /**
   * @param {Dropper} dropper
   * @param {Solvent} solvent
   * @param {Property<Solute>} solute
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function DropperNode( dropper, solvent, solute, mvt ) {

    var thisNode = this;

    Node.call( thisNode, {
      cursor: 'pointer'
    } );

    // fluid fills the glass portion of the dropper, shape is specific to the dropper image file
    var fluidShape = new Shape()
      .moveTo( -TIP_WIDTH / 2, 0 )
      .lineTo( -TIP_WIDTH / 2, -TIP_HEIGHT )
      .lineTo( -GLASS_WIDTH / 2, -GLASS_Y_OFFSET )
      .lineTo( -GLASS_WIDTH / 2, -GLASS_HEIGHT )
      .lineTo( GLASS_WIDTH / 2, -GLASS_HEIGHT )
      .lineTo( GLASS_WIDTH / 2, -GLASS_Y_OFFSET )
      .lineTo( TIP_WIDTH / 2, -TIP_HEIGHT )
      .lineTo( TIP_WIDTH / 2, 0 )
      .close();
    var fluid = new Path( fluidShape );

    // images
    var foreground = new Image( dropperForegroundImage );
    var background = new Image( dropperBackgroundImage );

    // label
    var label = new HTMLText( dropper.solute.formula, { font: new PhetFont( { size: 18, weight: 'bold' } ), fill: 'black' } );

    // label background, so the label shows up on various fluid colors
    var labelBackground = new Path( null,
      {
        fill: 'rgba( 240, 240, 240, 0.6 )',
        lineWidth: 0
      } );

    var button = new ToggleButton( buttonUnpressedImage, buttonPressedImage, buttonDisabledImage,
      dropper.on, dropper.enabled, { onWhilePressed: true } );
    button.setScaleMagnitude( 0.3 );

    // rendering order
    thisNode.addChild( fluid );
    thisNode.addChild( background );
    thisNode.addChild( foreground );
    thisNode.addChild( labelBackground );
    thisNode.addChild( label );
    thisNode.addChild( button );
    if ( DEBUG_ORIGIN ) {
      thisNode.addChild( new Circle( { radius: 3, fill: 'red' } ) );
    }

    // layout
    {
      // move origin to bottom center (tip) of images
      foreground.x = -foreground.width / 2;
      foreground.y = -foreground.height;
      background.x = -background.width / 2;
      background.y = -background.height;
      // center the button in the dropper's bulb
      button.x = foreground.centerX - ( button.width / 2 );
      button.y = foreground.top + BUTTON_Y_OFFSET;
      //NOTE: label will be positioned whenever its text is set, to keep it centered in the dropper's glass
    }

    // Update location
    dropper.location.link( function( location ) {
      thisNode.translation = mvt.modelToViewPosition( location );
    } );

    // Visibility
    dropper.visible.link( function( visible ) {
      thisNode.setVisible( visible );
      if ( !visible ) {
        dropper.flowRate.set( 0 );
      }
    } );

    // Make the background visible only when the dropper is empty
    dropper.empty.link( function( empty ) {
      fluid.setVisible( !empty );
      background.setVisible( empty );
    } );

    // Change the label and color when the solute changes.
    solute.link( function( solute ) {

      // label, centered in the dropper's glass
      label.setText( solute.formula );

      // rotate to vertical, center the label in the droppers glass
      label.setRotation( -Math.PI / 2 );
      label.centerX = foreground.centerX;
      label.y = foreground.bottom - ( foreground.height - LABEL_Y_OFFSET ) + ( label.height / 2 );

      // translucent background for the label, so that it's visible on all solution colors
      var width = 1.5 * label.width;
      var height = 1.2 * label.height;
      var x = label.centerX - ( width / 2 );
      var y = label.centerY - ( height / 2 );
      labelBackground.setShape( Shape.roundRect( x, y, width, height, 5, 5 ) );

      // fluid color
      fluid.fill = ConcentrationSolution.createColor( solvent, solute, solute.stockSolutionConcentration );
    } );

    // touch area
    var dx = 0.25 * foreground.width;
    var dy = 0.1 * foreground.height;
    thisNode.mouseArea = Shape.rectangle( -( ( foreground.width / 2 ) + dx ), -( foreground.height + dy ), foreground.width + dx + dx, foreground.height + dy + dy );

    // drag handler
    thisNode.addInputListener( new MovableDragHandler( dropper, mvt ) );
  }

  inherit( Node, DropperNode, {
    getTipWidth: function() {
      return TIP_WIDTH;
    }
  } );

  return DropperNode;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Listener that indicates whether a node should be highlighted.
 * Highlighting is typically used to as a visual cue to indicate that a node is interactive.
 * <p/>
 * A node is highlighted if:
 * (a) the mouse cursor is moved inside the node's bounding rectangle, or
 * (b) the mouse was been pressed while inside the node's bounding rectangle and not yet released.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/input/HighlightListener',['require','SCENERY/input/ButtonListener','PHET_CORE/inherit'], function( require ) {
  

  // imports
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var inherit = require( 'PHET_CORE/inherit' );

  /**
   * @param {Function} callback called when the highlight changes, has 2 parameters: the {Node} to be highlighted, and a {Boolean} indicating whether to highlight
   * @constructor
   */
  function HighlightListener( callback ) {

    ButtonListener.call( this, {
      over: function( event ) {
        callback( event.currentTarget, true );
      },
      up: function( event ) {
        callback( event.currentTarget, false );
      }
    } );
  }

  inherit( ButtonListener, HighlightListener );

  return HighlightListener;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Highlights a node by changing its fill color. See HighlightListener.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/input/FillHighlightListener',['require','SCENERY_PHET/input/HighlightListener','PHET_CORE/inherit','AXON/Property'], function( require ) {
  

  // imports
  var HighlightListener = require( 'SCENERY_PHET/input/HighlightListener' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Property = require( 'AXON/Property' );

  /**
   * @param {Color|String} normalFill
   * @param {Color|String} highlightFill
   * @param {Property<Boolean>} enabled
   * @constructor
   */
  function FillHighlightListener( normalFill, highlightFill, enabled ) {

    enabled = _.isUndefined( enabled ) ? new Property( true ) : enabled;

    HighlightListener.call( this, function( node, highlighted ) {
      if ( enabled.value ) {
        node.fill = highlighted ? highlightFill : normalFill;
      }
    } );
  }

  inherit( HighlightListener, FillHighlightListener );

  return FillHighlightListener;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Function for doing a linear mapping between two domains ('a' and 'b').
 * <p>
 * Example usage:
 * <code>
 * var f = new dot.LinearFunction( 0, 100, 0, 200 );
 * f( 50 ); // 100
 * f.inverse( 100 ); // 50
 * </code>
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('DOT/LinearFunction',['require','DOT/dot','DOT/Util'], function( require ) {
  
  
  var dot = require( 'DOT/dot' );
  
  // imports
  require( 'DOT/Util' );
  
  /**
   * @param {Number} a1
   * @param {Number} a2
   * @param {Number} b1
   * @param {Number} b2
   * @param {Boolean} clamp clamp the result to the provided ranges, false by default
   * @constructor
   */
  dot.LinearFunction = function LinearFunction( a1, a2, b1, b2, clamp ) {
    
    clamp = _.isUndefined( clamp ) ? false : clamp;

    /*
     * Linearly interpolate two points and evaluate the line equation for a third point.
     * f( a1 ) = b1, f( a2 ) = b2, f( a3 ) = <linear mapped value>
     * Optionally clamp the result to the range [b1,b2].
     */
    var map = function( a1, a2, b1, b2, a3, clamp ) {
      var b3 = dot.Util.linear( a1, a2, b1, b2, a3 );
      if ( clamp ) {
        var max = Math.max( b1, b2 );
        var min = Math.min( b1, b2 );
        b3 = dot.Util.clamp( b3, min, max );
      }
      return b3;
    };
    
    // Maps from a to b.
    var evaluate = function( a3 ) {
      return map( a1, a2, b1, b2, a3, clamp );
    };

    // Maps from b to a.
    evaluate.inverse = function( b3 ) {
      return map( b1, b2, a1, a2, b3, clamp );
    };
    
    return evaluate; // return the evaluation function, so we use sites look like: f(a) f.inverse(b)
  };
  
  return dot.LinearFunction;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Horizontal slider.
 *
 * Moved from beers-law-lab/EvaporationSlider on 9/15/2013
 * see https://github.com/phetsims/sun/issues/9
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/HSlider',['require','DOT/Dimension2','SCENERY_PHET/input/FillHighlightListener','PHET_CORE/inherit','DOT/LinearFunction','SCENERY/nodes/Node','SCENERY/nodes/Path','AXON/Property','SCENERY/nodes/Rectangle','KITE/Shape','SCENERY/input/SimpleDragHandler','SCENERY/nodes/Text','DOT/Util'], function( require ) {
  

  // imports
  var Dimension2 = require( 'DOT/Dimension2' );
  var FillHighlightListener = require( 'SCENERY_PHET/input/FillHighlightListener' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Property = require( 'AXON/Property' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );

  /**
   * @param {Property<Number>} valueProperty
   * @param { {min:Number, max:Number} } range
   * @param {*} options
   * @constructor
   */
  function HSlider( valueProperty, range, options ) {

    var thisSlider = this;
    Node.call( thisSlider );

    // default options, these will not be passed to supertype
    var defaultOptions = {
      // track
      trackSize: new Dimension2( 100, 5 ),
      trackFill: 'white',
      trackStroke: 'black',
      trackLineWidth: 1,
      // thumb
      thumbSize: new Dimension2( 22, 45 ),
      thumbFillEnabled: 'rgb(50,145,184)',
      thumbFillHighlighted: 'rgb(71,207,255)',
      thumbFillDisabled: '#F0F0F0',
      thumbStroke: 'black',
      thumbLineWidth: 1,
      // ticks
      tickLabelSpacing: 6,
      majorTickLength: 25,
      majorTickStroke: 'black',
      majorTickLineWidth: 1,
      minorTickLength: 10,
      minorTickStroke: 'black',
      minorTickLineWidth: 1,
      // other
      cursor: 'pointer',
      enabledProperty: new Property( true ),
      endDrag: function() { /* do nothing */ } // called when thumb is released at end of drag sequence
    };

    // fill in options with defaults
    thisSlider._options = _.extend( defaultOptions, options );

    // ticks are added to this parent, so they are behind knob
    thisSlider._ticksParent = new Node();
    thisSlider.addChild( thisSlider._ticksParent );

    // mapping between value and track position
    thisSlider._valueToPosition = new LinearFunction( range.min, range.max, 0, this._options.trackSize.width, true /* clamp */ );

    // track
    thisSlider._track = new Rectangle( 0, 0, thisSlider._options.trackSize.width, thisSlider._options.trackSize.height,
      { fill: thisSlider._options.trackFill, stroke: thisSlider._options.trackStroke, lineWidth: thisSlider._options.trackLineWidth } );
    thisSlider.addChild( thisSlider._track );

    // click in the track to change the value, continue dragging if desired
    var trackHandler = new SimpleDragHandler( {
      handleTrackEvent: function( event ) {
        if ( thisSlider._options.enabledProperty.get() ) {
          var x = thisSlider._track.globalToLocalPoint( event.pointer.point ).x;
          valueProperty.set( thisSlider._valueToPosition.inverse( x ) );
        }
      },
      start: function( event ) {
        this.handleTrackEvent( event );
      },
      drag: function( event ) {
        this.handleTrackEvent( event );
      },
      end: function() {
        if ( thisSlider._options.enabledProperty.get() ) {
          thisSlider._options.endDrag();
        }
      },
      translate: function() { /* do nothing, override default behavior */ }
    } );
    thisSlider._track.addInputListener( trackHandler );

    // thumb, points up
    var arcWidth = 0.25 * this._options.thumbSize.width;
    var thumbFill = thisSlider._options.enabledProperty.get() ? thisSlider._options.thumbFillEnabled : thisSlider._options.thumbFillDisabled;
    var thumb = new Rectangle( -thisSlider._options.thumbSize.width / 2, -thisSlider._options.thumbSize.height / 2, thisSlider._options.thumbSize.width, thisSlider._options.thumbSize.height, arcWidth, arcWidth,
      { cursor: thisSlider._options.cursor, fill: thumbFill, stroke: thisSlider._options.thumbStroke, lineWidth: thisSlider._options.thumbLineWidth } );
    var centerLineYMargin = 3;
    thumb.addChild( new Path( Shape.lineSegment( 0, -( thisSlider._options.thumbSize.height / 2 ) + centerLineYMargin, 0, ( thisSlider._options.thumbSize.height / 2 ) - centerLineYMargin ), { stroke: 'white' } ) );
    thumb.centerY = thisSlider._track.centerY;
    thisSlider.addChild( thumb );

    // thumb touch area
    var dx = 0.5 * thumb.width;
    var dy = 0.25 * thumb.height;
    thumb.touchArea = Shape.rectangle( ( -thumb.width / 2 ) - dx, ( -thumb.height / 2 ) - dy, thumb.width + dx + dx, thumb.height + dy + dy );

    // highlight on mouse enter
    thumb.addInputListener( new FillHighlightListener( thisSlider._options.thumbFillEnabled, thisSlider._options.thumbFillHighlighted, thisSlider._options.enabledProperty ) );

    // update value when thumb is dragged
    var thumbHandler = new SimpleDragHandler( {
      clickXOffset: 0, // x-offset between initial click and thumb's origin
      allowTouchSnag: true,
      start: function( event ) {
        this.clickXOffset = thumb.globalToParentPoint( event.pointer.point ).x - thumb.x;
      },
      drag: function( event ) {
        if ( thisSlider._options.enabledProperty.get() ) {
          var x = thumb.globalToParentPoint( event.pointer.point ).x - this.clickXOffset;
          valueProperty.set( thisSlider._valueToPosition.inverse( x ) );
        }
      },
      end: function() {
        if ( thisSlider._options.enabledProperty.get() ) {
          thisSlider._options.endDrag();
        }
      },
      translate: function() { /* do nothing, override default behavior */ }
    } );
    thumb.addInputListener( thumbHandler );

    // enable/disable thumb
    thisSlider._options.enabledProperty.link( function( enabled ) {
      thumb.fill = enabled ? thisSlider._options.thumbFillEnabled : thisSlider._options.thumbFillDisabled;
      thumb.cursor = enabled ? 'pointer' : 'default';
      if ( !enabled ) {
        if ( thumbHandler.dragging ) { thumbHandler.endDrag(); }
        if ( trackHandler.dragging ) { trackHandler.endDrag(); }
      }
    } );

    // update thumb location when value changes
    valueProperty.link( function( value ) {
      thumb.centerX = thisSlider._valueToPosition( value );
    } );

    thisSlider.mutate( _.omit( thisSlider._options, Object.keys( defaultOptions ) ) );
  }

  inherit( Node, HSlider, {

    /**
     * Adds a major tick mark.
     * @param {Number} value
     * @param {Node} label optional
     */
    addMajorTick: function( value, label ) {
      this._addTick( value, label, this._options.majorTickLength, this._options.majorTickStroke, this._options.majorTickLineWidth );
    },

    /**
     * Adds a minor tick mark.
     * @param {Number} value
     * @param {Node} label optional
     */
    addMinorTick: function( value, label ) {
      this._addTick( value, label, this._options.minorTickLength, this._options.minorTickStroke, this._options.minorTickLineWidth );
    },

    /*
     * Adds a tick mark above the track.
     * @param {Number} value
     * @param {Node} label optional
     * @param {Number} length
     * @param {Number} stroke
     * @param {Number} lineWidth
     */
    _addTick: function( value, label, length, stroke, lineWidth ) {
      var labelX = this._valueToPosition( value );
      // ticks
      var tick = new Path( new Shape()
        .moveTo( labelX, this._track.top )
        .lineTo( labelX, this._track.top - length ),
        { stroke: stroke, lineWidth: lineWidth } );
      this._ticksParent.addChild( tick );
      // label
      if ( label ) {
        this._ticksParent.addChild( label );
        label.centerX = tick.centerX;
        label.bottom = tick.top - this._options.tickLabelSpacing;
      }
    }
  } );

  return HSlider;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Evaporation control panel.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/EvaporationControl',['require','DOT/Dimension2','SUN/HSlider','PHET_CORE/inherit','SCENERY/nodes/Node','SUN/Panel','SCENERY_PHET/PhetFont','DOT/Range','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var Dimension2 = require( 'DOT/Dimension2' );
  var HSlider = require( 'SUN/HSlider' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Panel = require( 'SUN/Panel' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Range = require( 'DOT/Range' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );

  /**
   * @param {Evaporator} evaporator
   * @constructor
   */
  function EvaporationControl( evaporator ) {

    var thisControl = this;

    var label = new Text( StringUtils.format( strings.pattern_0label, strings.evaporation ), { font: new PhetFont( 22 ) } );

    var slider = new HSlider( evaporator.evaporationRate, new Range( 0, evaporator.maxEvaporationRate ), {
      trackSize: new Dimension2( 200, 6 ),
      enabledProperty: evaporator.enabled,
      endDrag: function() { evaporator.evaporationRate.set( 0 ); }  // at end of drag, snap evaporation rate back to zero
    } );

    var tickFont = new PhetFont( 16 );
    slider.addMajorTick( 0, new Text( strings.none, { font: tickFont } ) );
    slider.addMajorTick( evaporator.maxEvaporationRate, new Text( strings.lots, { font: tickFont } ) );

    var content = new Node();
    content.addChild( label );
    content.addChild( slider );

    slider.left = label.right + 10;
    slider.centerY = label.centerY;

    Panel.call( thisControl, content,
      { xMargin: 15, yMargin: 8, fill: '#F0F0F0', stroke: 'gray', lineWidth: 1, resize: false } );
  }

  inherit( Panel, EvaporationControl );

  return EvaporationControl;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Fluid coming out of a faucet.
 * Origin is at the top center, to simplify alignment with the center of the faucet's output pipe.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/FaucetFluidNode',['require','PHET_CORE/inherit','SCENERY/nodes/Rectangle'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );

  /**
   * @param {Faucet} faucet
   * @param {Fluid} fluid
   * @param {Number} height in model coordinates
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function FaucetFluidNode( faucet, fluid, height, mvt ) {

    var thisNode = this;
    Rectangle.call( thisNode, 0, 0, 0, 0, { lineWidth: 1, pickable: false } );

    /*
     * Set the color of the fluid coming out of the spout.
     * @param {Color} color
     */
    fluid.color.link( function( color ) {
      thisNode.fill = color;
      thisNode.stroke = color.darkerColor();
    } );

    /*
     * Set the width of the shape to match the flow rate.
     * @param {Number} flowRate
     */
    var viewLocation = mvt.modelToViewPosition( faucet.location );
    var viewHeight = mvt.modelToViewDeltaY( height );
    faucet.flowRate.link( function( flowRate ) {
      if ( flowRate === 0 ) {
        thisNode.setRect( 0, 0, 0, 0 );
      }
      else {
        var viewWidth = mvt.modelToViewDeltaX( faucet.spoutWidth * flowRate / faucet.maxFlowRate );
        thisNode.setRect( viewLocation.x - (viewWidth / 2), viewLocation.y, viewWidth, viewHeight );
      }
    } );
  }

  inherit( Rectangle, FaucetFluidNode );

  return FaucetFluidNode;

} );

define("image!SCENERY_PHET/../images/faucet_knob.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

define("image!SCENERY_PHET/../images/faucet_knob_disabled.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEUAAAB0CAYAAADTq/8hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QcUABEOu063ngAAGdJJREFUeNrtXWuMJVdx/qq7753Z2eVhEyBgHkGRExuJiMQJBBElkcgvsBGSLWG887izdxAiysOgKApSIqEIfkSRQApxjOS9s3N3x+O1YYOCZRLlBxFKUADlIRwgCY8IGwjGJibe99zb3ZUfe6r5um71ndn1mlhRrjSa++jH6TpVX1V9VadbcBVf99xzDwDIO9/5TrXvjh49WgB4OYAXArghvX++qr4EwCKAXvo/FZEzIvI4gG8B+DKArwyHw0fsWKPRSADocDjEM/mSq3GQo0ePYmNjgz8vqOovAbgZwE0AbhSRF+x1PpGZn59Q1S8C+AsReWA4HD7O58yyDEeOHHl2CWU0GkFV842NjSoN9OcArKvq7SLyY3yxqo3yqKpK+qxpDHppMwEAzbIM9llVbd+LIvIAgA8Oh8OvpvNnw+GwflYIZTQagVV4NBrdpKrvV9WbRaSZcVVFXdeo69oEoXVdC2kFC6W1X5ZlyLIMeZ4La5CqliJyd5Zl71tfXz8XjedHLhQewGg0OqSqfwJgPQ28VlWtqiqrqkpolmdMhP+zRtB7ExhEBHmea57nVZZlRTrmt1T1zo2NjT83vMnzXAeDwdMWSrbfDTc3NwGABXKzqn4dwHqWZVBVnUwmcvHixXwymaCua7tIFZFm5u19+q/pvfLv9tne13Wt0+lUdnd3i7IsNX3/cgCnRqPRR9O49GppSnGFwPoeVf1QGni1u7ubT6fTBgcSJjSzblrB3/vfIm0y7bH9VBXT6RRVVaHf72uajHdtbm7e2Ov1bllZWTk9Go2Q5zmejsZk+9USQ/nNzc0TIvKhLMtQ17WeP38+n0wmACA0u2IzXxSFJGxoaUl6L7a90yAh7RL/uaoqXLx4Uaqqsn1+uaqqrx47duz64XAYCvqqYsrW1lYj9dFoNBaRVQB1WZZy4cIFAYA8zw0oTRhK3kPtogONaHkf+ilBCkR/CEpS1zUAaF3XYqbZ6/Wk1+vZ/t8XkTesra19fWtrCyKCtbW1Zw5oNzc3P6yqd+Z5jslkgnPnzinNpMcEkBT4grXxuz/EG75wloodQxmw03tJmKV1XUuv19OFhQW7njMAXjMYDB7Z2tqSwWCgV00o29vbKMtSBoOBbm1tDeu6PioimE6nev78edMQMxEWxoz6Btc8gy8GzB5bvAezzyykqqpQFIUuLi5CVQXA10TkprW1tTOs6U8bU6qqwmAw0PF4fL2qHjUMOXfuHAtE8zwPgdLjh8MMuNijAV0SlvKx5v3leY6yLGU6nUrSvutV9ZMAMBgMsLW19fSFsr29zbZ4wrDh7NmzYrOc4obGbfJFWFRq7yPBpOPMuGQTiH1Oglfvxm0/O2dRFJhOpyjL0kz2V48dO3bXlQgmFIoN7vjx478J4PVZluHChQtSVZUNUL2Z2IAtOuVZpuhWLcr1JpYuUE0g3vSSMNR5L3jhT6dTqarKvNqvj8fj20wwTxtT7r///qXJZPIEgKXd3V09ffo08jwXwxESjKksu1YDUCQcFY8X6cI1YQDnPiDBNilAcmLNPuaJ6H2DS6qKAwcO2BguAnjlYDB4fDwe78sbtTRlZ2cHOzs7AIDpdPr7IrIkInr+/HnxMxMBqtcOTgQ9DtjgzQSjTNkfi7+nyQkxbDKZCIBKRBYB3GOCHY/Hl6cpJ0+exO23346dnZ2XqerDAK65cOECnnrqKfR6PbXkzNmzmNZYAJfneTOtVVWpD1IMVJOpiMUevEkSHCeLTWYdaQ1ribnroiik1+vZOVcHg8GJBAtYXV29PPPZ2dn5XVX9I1XFk08+aWF1M8MOYMUDqUWwrNKm4oQ/La0JXto1vqqqQldeVZUXDBYWFkzw38nz/Ibl5eWzVwS0IrImItjd3dWyLJs4xLDAq7W9J2G1zMaExALrMo09VTuZTjSeCITLshQAUwDX1XX9PgAYj8eyL6Hcf//9kv6/CcCrRaRkLOELjBI30wyPN36QXqgRpRD9cVI4L14x4bMm1XVdpIl633g8vm5tbU23t7f3pSl5OukAAMqyzKfTKYqiaGIJN3AlT6Lud447lC8mcLOt2IPPRb8pgavSsZQny/2ZR9SyLM0Tioj8AQAsLy+jSzCt6fr4xz9+TVmW/wbgRWfOnMHZs2fR7/dDj8Paw+bFnsEwJQr153mwKA9ibaqqau62dV03+GIg3Ov1tCgKUdUyz/OfPXz48Je6qIsMAE6dOmUX8EYReZGqYnd3t5E48ak8OA601EWuvK12uU7bh49NIKvkiZRwioM7jczQRb5K2KIiUqjqu+ZhWQYAt956q32+RURQliUIYOWH52sRQ0ImIkYbcBBn3AgncHYMy7D99sFnYb7FxpG0URjgmbPJ81z4v11Xwpnf2NzcfMGemPLggw8uicjrUuCmZiIMbN5surxDNHusLZxEdrjjfR2bAdjGyt6NhZR+b+KeXq/3LgA4ceJEt1Amk8mPi8jPZFmmu7u7wijus9oo2vRewiV4e3qbru+6vJ7Pq/z+fhzkiSzqW3nggQf6KysrM4CbGZ4A+BUAWVmWZVVVrQCN7Zc9DCeC3vswvnRdbJQlm4ex9z4B7fJyQSjQwhRz15SM3jCZTN5qnqglFMKMmxKyZ3VdoygK76HEnZSjd+HPwQ8zA3fYIY5AEn7PpQ7CIfutpdH0WYgZ9BNiE/22EFPIpl+XwEi8uXhVdZzITPYbcSddjH5XohkFgT4YjEyc8cRjik10IrxVVd+xvb39Ao8t2a233qqf+tSnDqYoFmVZigNWdS5YjTdxZJGSGamv4SRhqhOqkok2+3rz4+M6963B7Deu24pofIw0dqhqLSKZqr41CTtrAW1ZltcDOJhiFXEeRsgkhEzFlzT87426e5WmsFzIvUuWZZJcqJAmCJU5xLtrHmeXaRZFwce13cy/rxit0BKKqv60iKCqKhiekLaoB1bWINpmJnhjajJK1mwfz+R5MKXtfEqgUYJqYE1sYCuoS57IhPDae++997qVlZXKvJCpzE9ZUFXXtXosYH6UVHHG/fkLCLBD52BH69gB0a10oREhrn4fx+u29iczuwbAm9gLmVBeRTyF+IyX3jdqHAV19HsrB2JnFDH+Fqmm4pd0BHbi93cALx3cbssEOWmkZPXXouDtFWY+vobD0WIUPAWxR2dgF9EBXeWPLq/kI+Mo0IvG1VFRsA1+fjweP6cpsD/00EPPK8vyxWQ6wuE9xw/etfqTdGlYF/PmcyFuxdiLfLJt8jxvsW08Nn7x+e18dV0rgFpEbszz/FUAHt7e3kYhIs8RkWsMgY2J78g7Qpu2wXlQZR6F1FU6ugx0L3qSq4JRXTrPc/ExjVUUWOiMKUkwFtE/DEAyVT0E4LkN00QHDlh2drtRv0mDKcypMCZ5NSbcabl6/+fxKjBF6TJRw0HKnFtYljTtjSaoAsAhAIeoF6Sp2ZjKMZfRlZSx+2N8cbVgtUG47LUplEUMHR/b15KdpnXZmyZzaWXTSdhZOufrTeMKAC+ygRr/4BkpGlyrYOVxhDSoIYLM5s2z+fSecxpHMnWxhMo4RRMmUZUznU+8DdZ1bQmipFLrK7a3t1+xvLz8aKGqL/YuLKL75nUVMFh2dTIVRTFTGPPkd1RAi6iBqHMh0JwZyoG9aJ7nTamEPPFrADyaAXgeq9g8EqnL1TL75TLVzu0j8mken8ICnufeu6oFUb3JqAkS6o0AUIjIks9+zf7mEciRa+6KX3xRnc91JS7ZjmW/+4J9VA7h8/KYbDITTr3aGgEPmCCJ4JGoEyHIUXzdRVlrAg3R5PZDUmgvUj8qvZJAlRNEq0X57NyYfg+4SQmuv+uuu4qCzAdFUbTcYETzRTMwh0Sa8WBdpuFAs7Nb0mODXTx7lq4CP4O/lT64Rw/AS5aWlq4tLEahCxNu1azrWlwP2wyDZZPE6mnH4zYMfwyLLWiS0dGOEU6KVRS4AE/mwQUw3y2lSZji8OpaEXlu4QK3VseARbGu+h+BmwZBmaQTq3Vf27FcZitmOowtNNPqNYW7I30fjG9xZ8IqOIdSw4AAuEZEri0ALHhNmVO+EM+zRDPPQqJ4oKUsQdIprqvJa07TgmrHc8JrsIqDQ4tTfKmVOxjs3Ol4LysA9Nl3m4QtXnEn0DkpAPysBPUi9YLxszbH+2jUvU3RbZNXGXCyIFww18I3whSIyEsLXFqA1NIUTzLxbHaURpu3XLIIaAfpohiYse/wRPa7elBNwaZ4Ej2Klm1/F2MJpSDXFLi0MstnwtLRjqXzKnaeNfMpO9t9EJhpFBt5WtMEwt7KaAC2eyd09QGcpR62b6o3Q0Se32gKs2c+M05Vewk4lFYDoI9SzZ7JLQuDOnkGIfPt9D6cS3mymb2MI5CE45Y0URZPNf39pGUvLAAsOEyZmaGu/nlftePuSLRXerW6I7n8Yd6HF0cxdrFwqHRhYcNMZuxculD/f2f10h3juUWU2EXZqysneE8kAZfbuM8oeGMh+mtzg5zRXAJ/4fjDH4Oz5/2URdLrYBH0ncyE377X1c8glSRmFiZ4HPHdULYogclw0hSJNIWCxZYWuIiX4xQJGD6hYhpTsAczAP2unrO9FhnM6xqY1wsbpQj7ffmoea/s2nO280qy6bVYWK8bgaOPaOGiW3CQFPTFtvCJOQu/9odbSx2OhHGKz6XcZ41SkaA4F3FEymWWjM0n6gGJVNMD5px2CHSA4Nweunmz77UqKG7tq3fOe1Lat58BuA7//+LXq1qa4msjEcXH23A3NbeBd5Ui5i1m4i7KYCVYWL9hsmg/Tcqe9OJ6Ee1bNd7HLoxQvjVAa6ChaFI9R8qrK4zndRfJNIKfBD5vGOZTbBIJT3xo4Y9t2XQwMUJjqTMANc+iXnqF9pkiQbVt/EymLzXSCL5YrwkmSNcqGjFvLbB1gtFoaZ2Nm8cWCJTPOy18swtL1c2ABOqsfpb9gFyuIy4TlkBLfDVC5oCmOGZj5jdfsLf4xmsumdCk6LgQv5pLLHTv0KjWSfieBmm71kVzc7ClEEwQ7RXm2/ukudKBX60w31EMXrt4Us8XAEq/ZGS/Pa4B5rRYcr/MxDTEui87TGzP80dAzMf31xAt4ozMKL3OFLh0M5ewVGATxlwtlTmbJcVeE2xbpg79RVvFkOu5BHbi0ggGZ6GxyhxBCedKfE7TRKNJyckAwGNFWgszYzb0Z0ycz0jVq6QJo8t8ovqv3UKEiGZPGTbWZF6Pz2PfB65badVaq2xKQtC0r9j2qvpEAeCi9wI2u16FjYzpQnk7CZNAbhVHk1Ta73S8mVaLiI6M4ho6t0SReLrg0HySxnBS/J8FLt3BZobEZWLITML6UAOhiNc0jl3oeOLCdYliozkepiUU51nEBZgtWoKhgTTLmy0APFIAuOBLm3sBnG+t4P09VWjC9kDoz2EDZSKb6YRAIOH3TJ+a4/BxTV3XWlVVa+kuXeM3CgBPsvcJsEWJ1+D2cDYVZXAzV5kqAhppUtrXjtlgUBqDzqEl+Xgzxw7u1NMKJczE6bZIoLoUVPVMAeC//UAtvfcSdj4+asVQf+HOHPgWIg1xTLTCvLKpdkSrDeVhfLAvm9qOpsU2LusbpomfqOq5AsD37URlWTZEM5uAj05dVMimZ6VL7Yg/xFOSUV4UFdNZU7z5JPMUbgRkUzTa0rTDL9Ul8/mWqj7GQmnsLCo1eG8RXbTNPC+NiTJRC+4irNpvK0aEJR2xEPyyf6sVBUL5l/X19ccLVf0+Z60BGoept3eNXS7PR7ORkDtcbEg9RGPgzz77DsIKVFXVCMW0LF3Do9afYiu6W1oSVPXEzaYFXRARMe1INi40e9LVi0sqL74uzKRy8FvL3UfhABcgXMzSjLuqKmHPCOBR62T6ry4SaV6eE5HJrD2uaNWosg2AzWseETUv5+kKIXg8BsTMGfESXXe+z5mmPMHmw4P33iVSWdueyWyOF+zkHgMcWT7TRdDVTrYXuxcxdIwpdo2GOSmaFQBnReTzJpQzKSnsceRHB23VffhLGrTdLEa4u8DQPuJEOPXneMOvTOe8yscmvi/XCbUVXxFeqrt9o43n0ZWVlfLEiRNZISI/SNryUpMet3dGZA3jgWXQrtlGqO1UumaUsuuZY0cVwsB1R8TXzLg5LqrrWiw+SYKuRCQH8Dc2wZmqnlbVJ7lNwZ1APalkAQ9pi1pSxTPARA7bc8Dc+W2UjtGiP+ctkaNgzLGfLaJaKVZREanS8e5rWmff8pa3PAXgcavN8sCT/bWg3LdYEaILpQmt3zzTT+cQpgXoT9x7iX6zc/rYysZt29s2CUOEhCQAeqq6q6rftIVQRkd+x1oVbDm/j0WiBUaR2rJH6AJo3nc/cUoXGziv17crtnKgbnj36ZWVle/4RVD/4fvou2g/H4z5zJW1wqlzKBQ/y3PKpnMvvus4TgNbf9SR8Hetmnn6/xXubeMLsWyY/jeY4ryI+n0iLHKmpO4/4xXovP5za//gfkyGSy2mjT+T4CYGsrawskg/ftXMx9qcer3eTKmA3V7yOBrhjdtWorDb9Zf4QpbQag1xq8sk6qFj7bXkNIqoLaKl5r9vHj58+O9PnDhRLC8vl41QiqL42nQ6PQfgYJZlzK9GDb1R2t7CnSg38lhDTN/cSDoK5qLAsut3NmPn/SzG+YRvU81OnTolb37zm8+ZCRVF4blQdm1q7pLNxb6zSpxXcQoI1d1xS4nvUPdquVDyyxGpjaD5yPIb5WNYv5uI1GmC/tQ4tqYdn8DsC6r6C0VR+FhFuLHILsoaAS0B9N7HJ23elLoqhBHj5rWBa8B8MwgeG2m7+PA+z3Ot67rIsuyzy8vL3zZX3AiFRP2PaYc6y7LcXHNUxQuabFp0pL87ncMBf+HCjFxAMqkvsFNor4GgGawbqtMtxbMT7USDyujWQ58BUBdFUdiqqSABVF9E99FiVMT2pVbvRslbaFVVyjPLlKEv/juP1vmbHbeqKqV05DER+QR7He+S0e/3H1PVh+u6loWFBeUYg8ypWW3aUboUruAFHK/MCQBt9bpE2Xg6rkSZsuddmCizqDbVw9lj/tXhw4e/u729LTM3lbE3t9xyy3lV/QIA9Ho98R5jThGqk/PwjTVMGO8nQNvr2P7imf4gVg3EOTdAXZblBxKWzFxI65ZmAB5MHghFUaAsy5lBuFilJSSf43huhY8RdT/t9cfnMk3muhILgfMdO09KX7Isyx5aX1//RpfwW7c0y7Lss6r6uIhgYWGBJdxqnHHR7AzmRLMZRKozZc+uph2qP2nUhuYybo8nqOtai6JgwP/jeRrJ944sbrvtth8A+GtVxeLiovrZSWgurijG7Jq4+ymF7V3z8Ci6lSp5H/EC5YzXmylPKrXCf3J5efkzEcDOCMWCFxHZSlFu1ev1UJZl5F1mvIj3PlGC5ntl/Ww6HsVHoY1nCnramlzHR67JbHjJ3PtNIB5gEa2QsNd99933ZVV99YULFzTdoFfSStKZm/SmNUCaBCm0xKXVLsGz7m5CMxMDIbidM6/38Y/C8RwsJYsiIuj3+2UKVE+urq6+4/jx49nq6mq9p/k4Gx6nG9xKURTCgOvV2rU0ILpfAQNexKRfzl0BnbmGGTJrWcKSAsBERO4EgHkCmRHKyZMnQZHeD0QES0tLrQuJWqWiz14AvlTqqclI9bs+R7FOdNfitKTYBvyHKysr3wMu3c5530Kp6xr33nsv7rjjjm+r6kcNcHu9XssFRqWILo/TVTPiwXsyKvBqoYZGmkctHfaMDqjqP6ytrX1wNBphPB7Pvb91Qx3Y64477mje93q9D0wmk98GsLS0tKSnT59ustxoIbfvNoooSbjFj9z+EXU0+BTDm6TviOAcp9/vswteM2bxsm8R39jOzg7e/va3n1fV36vrGv1+XxYXF1skcRdP6ynLLrPw2NPVuRjRnx1MWzOOdJNySxp/azAYfAXY/wMFsnnVtdXV1Y8A+Hxd1zhw4ICmNXdNPBHEEEL5UYs68M/tcbkQxz/Ct4u3m+4Rqx9WCkwr0h2K67TNQ2trax8BgP3cL3+uUJaXl/kgK1ZEP3TokEWUlnWy++NoVwMiqcWvEjmFgLxSn+EyScSxiwm/LEvt9XrmbTIA31xfX78ZALa2tuRynvPTuSQrz3M72NdEZMPIpIMHDyqAVi3FxT0SqTnxozMMvy1fYy2JIt6uaNmqmr1eT1U1U9Uzdjuh9Ciby3rGz/+lhx4pTfJrB4PBF6/0oUd7Lt6zR8AcOXLkPQCOp4CoPnjwILhwXVUVyrI0zAmz1DmPiGBNaGka7T9zbABYWFhAegpUloT+i0kg+7kny5Vrin+QmqouGxG9u7sr6Xkbfsmb8F01/B22ooczBvSEREtt7FE1/X5fE2VqKcUb1tfXP7ffp7NcsaYkLWmeQ3jkyJEVVX2vYczS0lLV7/cbD2GzbRpRlqVQgCVOS4S1hLJkznhb39d1LXmeY3FxEXme1+m3R0Tkpevr65+7++67LyttuGKh+NfGxsaHReSWuq6/V1VV3u/3ceDAAeMsWj0grhTio9CZCqHPeNnjpKBSFxYWrFSSA/jLqqpuWFtb++6xY8fw7ne/G0/3qZXP5sd48j6aHrhUZ1lWq2oB4IKq/s7GxsafpbFctcd4/qgf+Oq7KaOn4FqjMNKdjP0qDIjIx0TkvUeOHPl2NJ7/FaGwcH6EjwaGiPwtgDuHw+E/pfMXw+GwxFV+PdMPkX4bgNfi0k3Fr91zMAGzr6qfB/BpERkPh8N/53M+Kx8i7V8djxvPAfxE6qn7SVx63PjLVPWVuHQzzwVcutdcmR43/gSAfwXwzwC+AeBLw+FwmjQjU9WaJ+CZeP0PvGe6mo3SELUAAAAASUVORK5CYII=';
return img;});

define("image!SCENERY_PHET/../images/faucet_shaft.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG4AAAAWCAYAAAAhKqlXAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QcUAA0GU+Ji8QAAAsNJREFUaN7VWlty4jAQnHapWFIFV+DE2bOyVVCWSBb3fpAYYkvyjCyRrH4gds9D6nnZBK+vvykfizJ+vS/O/4igZthRFyUtQ8bNJHDMoyb2dFgVbnRjJfbjEoDxEgDZbDbSdZ1YeHCHw0FCCGUbj+mmigpZ5N5ySFZ8xkcaycrd1xBNDjcCIz7k5N1utxPnnO4wNfdZRlgNAuN6WBpDdw0F8jQKWfFu+/IieEjTGBkoUFxzzW3zCTa+R49W3m232y/1tfXGWusm65Bb7BNZZP2z7Q2DQhIibr/fyfv730XsTSGXNU4LtqLk1SWuXYlKYhPXq+hOYN1ut5fr9aoMJEqamYU4I+sXONLiQbuMSfjRijgREfdrs1ERd+MKC8fzcX1o0YWe3FdXDkesEIS5uuoAqHqcUuPto4s7jrWH+MSFb7a/mHHkIMMw1I3SVJubHAFFxy6KKS8/dWpZfUoQzQ263ntdqVTsAKgc6lO3Va0VIuAI1rqEmfNcHAFRgUFkD43RoQ8CccfjMTpVooAFwBaOeRNYgWdcPhcAE+U5UgBd6mnOMIdJ34K44/GPXC5vMyBZmkVYlX3aQ8kGTGKCnZEBxYFm6j8E2SqADiszLi3jTqfz+K7yUYdNnxbMHNU3xFiGWClgImMG75tEtbJWiMd997QQ530v3ntjWarbrWHVZYIgLff4bIpU34Zt0MGDHA3BPAYRVDIuBC8h+NoTRdsBDKikm5FMNQwpUde6IrlPdKfMGOd9kL737WiqwNwse6pFA+99D7ViCk+Rd5cQJIylkqboiDXrFhnGyabq2cCXEbsJccZWTVDlj+u9l95IXEonWj2Zop1+AG31sc1eXQheLmOPQ8sZxPRmoIVNy+PAjwuAKXHn03nFm5MGhEF+7Mr2Q9qzyvzk9PjmZBiGkV3tTwvVfi1e/KeY/2+h0uPMEpn/AKUjR8ldt3aTAAAAAElFTkSuQmCC';
return img;});

define("image!SCENERY_PHET/../images/faucet_flange.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAA6CAYAAABMOlKBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFQ0U0ODkwMERGREQxMUUyQjU0RUQyRUExNkFEQkJDRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFQ0U0ODkwMURGREQxMUUyQjU0RUQyRUExNkFEQkJDRSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkVDRTQ4OEZFREZERDExRTJCNTRFRDJFQTE2QURCQkNFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkVDRTQ4OEZGREZERDExRTJCNTRFRDJFQTE2QURCQkNFIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+xNqcXwAAA+JJREFUeNrsl0tME1sYx8/MnJnSB22npVMoUloqBQuIKAuBKlJBoiYufMQHGt0YTUx8340aTNSNMXdxb25u7kITN8YYNy7caNTE+EjE6MaIz8QQiAvwxbOdmTMznjO1imgQoTNNrn7JWcx0en7ne5zv/A8AJlu0cUWDqUCby8t0nLp0lTYT2rB+915faE6badCy+kRVrGVNpzQ2DGiTwkrHN//xt6aqTk3TzIE2dRw4xAfCCUUW9WfDoXPbNyViS1YfJWHNmKHQQEVdoGnjvtNITMLx7w2D2nkft2zXybOQywurCgKGQ9k8G1i5/69/nEJxG5JS3/yefShFgeZth04EKuZvl1Nj3/2Ezi6PAq07jh2tXrrusDiucCYazGIOqdYdx/8M1TXvE0eHJv02K1Dc2gradp44449Ur0r9ADhjKM1AgEO5vHHDnn85qyOUGh0kQTYGyrAcKF/YXlO3cmunEK5aSypUFpNTAk4diguEYS3AO6usMLKgpTlSn9jsC8fa8f5jUWok7TU19UVDiCejaL2IWXdh0MFA1gU5Cy+UVgqApov9ocqgNZ+PeAKhcofHP4ezOZykh2ZgkPr5SMEj57u0zMNICvXTDOvAjlkxWJ9OxacCUFWgIBmQzqImh/UgsjQ17VqASKWBpqnpTsJQAtBkAAgnKX2dRzKo7GwvSCPxq4m/5NG4gwDOJEwzgIIcQKlcQH+Z8HJUDjzNQUpJc9ByAP3Ujf7/niq5gMo5yekvBFV/h9c4KKDZz3JFJaHW0ovQNOMWAyH9JafDkvKNMNMXhIdCRJmiZGUxFMTCOSNBXUKJHUtQN5alvK9kti5BfSXREmu+K8L7S8vtvBDjrDYsQSWAZGn60CmLbcgBbyBcGKqNLw7NW7zFV1qRFtuSaBB04rUCL6CsPlEzt3VDZ0Gwci3C4nvibXvS/08HqqkKeNf3sv/ZncsXRwffdAmz5zVBi9WNEM470cw/GMxMCoIU2ZueJy/7urvOeYKxSrs3UCHjcJNbwWSDycYWSA69Het9dPuCwxd05heVNci4yAzzdLwhfFXseXDtisXlA55wzZK0xwZDM/b68d2bFmcB5w5VLdILzKjwTrSBFw9v8OHaYqu3aAEitz2jPdXbKd4+/U/uXRGq4wuhzRXRu5nRUGKKlFQG+55fL6ptWU3RDA/GtU/DoMRSH/qHkTj2yF8d71CRTJsCJTbY+/SV1VOouYOxBAabAyX2oaf7tlDVuIizu8Kkm5lyfZJG3qvPLv+3G59mQ/rhYZZaGB3oHchzFci4cSwzDUrsfU/3fXdkfhxGo1HToIqYRPDVrYMfBRgA/5fD3M7UA4sAAAAASUVORK5CYII=';
return img;});

define("image!SCENERY_PHET/../images/faucet_flange_disabled.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAA6CAYAAABMOlKBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFQ0U0ODkwNERGREQxMUUyQjU0RUQyRUExNkFEQkJDRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFQ0U0ODkwNURGREQxMUUyQjU0RUQyRUExNkFEQkJDRSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkVDRTQ4OTAyREZERDExRTJCNTRFRDJFQTE2QURCQkNFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkVDRTQ4OTAzREZERDExRTJCNTRFRDJFQTE2QURCQkNFIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+aYD33AAAA3pJREFUeNrsl99LmlEYx19fXy3T3imZmeaPN9HpRj9oEMaWk35sddGuTINNGNWgK9lgVxsUg93uZoxdbTCIXYzRH+DNYDAItosuFgUhBFE3rYvStFLTPY90orl+qe97BqsDL7w/9HzO9znnPOf7MAzl5vV6u1iaQJ7n5UNDQy+oQgOBwGObzdZPDdrR0XHd5/NN7uzsMCylsLIjIyOv8/k8DxcdaCgUetbY2NiTyWQKz5JD+/r6eiCsUxhW0iSFOp1OUzAYfJdOp7mj7yWDarVa5cTExAelUins7+8zkkOrqqqYSCTyRq/X94PKv75LAg2Hwy9dLtejvb29Y79zYsJkMhkzOjo65ff7n6dSqRN/x4k4h7KxsbFX7e3tT04DigaF1KYfHx9/39zcfC+ZTJ75+4qgcrmcgVAODg8Pv1WpVPbt7e1CiCWBchzHdHZ2tgwMDEwKghDAFYqL5jzAkqAKhYIxm81GSNy34Xpgt9vvwv5T7O7uHi6icw8aO2PZws5RNDQ0aEDFFXing3kyQEdm6NxaW1vrgNzp1Ol0HggjjzmUbIeD/5YWqenp6Tx5gFGvA1QDtyrIJIWh53I5Bk+GbDbLYGYhMJzPchuHHeJ10JGB3JOw/ZFJWHFyCYdKxO70TGglYSobSkvdv1d6ccKLKY12oy+zeJ9Sgxabpv8XenHCe7mQLsMr2ilD1B5VTWyLJFC0juR4A/+6DvcaeHdozIiHQmNGTFrFNQ9a0APPqjAYDGo4dbQA1DU1NRUsqNVqtWg0GofRaHRCvXKturqaxwGQUr4saClm22QyGVtbW31tbW1hGEzBbB9Xf4oGLS4rwOW39Pb2TgI8gKpLWZBleRWc27W1tfXZ2dnPm5ub3x0Ox02ovrUIJ2vgtKsig4QdrKysxBYXFz+CYnddXd1VDLekUNLi8Xhqfn7+U319PQ/1UNdZikWzgljjzM3NRXmeZ6Do8p8GFt1/LiwsfAWw0mKxdJ8ElsT0xmKxL6DWDKXljePAkkBx+ywtLUXdbre3pqbGgc9HoZJZ0EQikZ6ZmXkISpeLDb2kNcXW1lYCFthPj8dzH9SyVKDYIIksw9zmIZf3kKxFpXqCBPIN5rdbrVYLmM2olBXJZDIXjUYjcGrFqSnFtrGx8Qv2b8Zms92hWpxCmH8IgnCLc7lc1KBwGGRXV1ef/hZgAEjL4YVbo8WlAAAAAElFTkSuQmCC';
return img;});

define("image!SCENERY_PHET/../images/faucet_stop.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

define("image!SCENERY_PHET/../images/faucet_pipe.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAABUCAIAAAByYpLHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2RTYwOTU1Q0RGRkMxMUUyQjU0RUQyRUExNkFEQkJDRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2RTYwOTU1RERGRkMxMUUyQjU0RUQyRUExNkFEQkJDRSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZFNjA5NTVBREZGQzExRTJCNTRFRDJFQTE2QURCQkNFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZFNjA5NTVCREZGQzExRTJCNTRFRDJFQTE2QURCQkNFIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+TigapwAAARhJREFUeNrs2cESgiAUhWEQ1CZfOR88BG7UFmpa2d/MYVkLv849YKP+tu9m5n69vPdxu24UynpZKZR5XiiUZcFQ5hgplGahUELgpMIZ0DRNmFRCwHQlBg2IvINAqYgypngzf+Ylx5871yjh5F//7qu2g4JjrHYLAlFmCgU0oPaHkjOgqAF1lHbaUigfzpzTUxFlQHEkisPUFmOh7ORnKuZMqfS1NQylWsV0xQyTSsWkUgqIkimU+z1RKMdxiNJRUkqc2hbKk4ScM4dyKJUBRV3pKaAd1O7MnFSyUulTyerKIBVSbTWgEUWpqCtfUkpVKuSulAI6+IuOOPbBL4qOuL+j2Gshnq+AKJwBgV43iDLsCuQ9c2M8BBgAKbAhm6H/L0QAAAAASUVORK5CYII=';
return img;});

define("image!SCENERY_PHET/../images/faucet_spout.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKoAAADdCAYAAAArUen7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2RTYwOTU2MERGRkMxMUUyQjU0RUQyRUExNkFEQkJDRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2RTYwOTU2MURGRkMxMUUyQjU0RUQyRUExNkFEQkJDRSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZFNjA5NTVFREZGQzExRTJCNTRFRDJFQTE2QURCQkNFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZFNjA5NTVGREZGQzExRTJCNTRFRDJFQTE2QURCQkNFIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+C9VhZgAALp9JREFUeNrsXVuM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';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Faucet with a pinball machine 'shooter'.
 * Pulling out the shooter changes the flow rate.
 * Releasing the shooter sets the flow rate to zero.
 * When the faucet is disabled, the flow rate is set to zero and the shooter is disabled.
 * <p>
 * Assumes that this node's parent is in the same coordinate frame as the model-view transform.
 * Scaling must be done via options parameter, eg: {scale: 0.75 }
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/FaucetNode',['require','ASSERT/assert','DOT/Bounds2','SCENERY/nodes/Circle','SCENERY/nodes/Image','PHET_CORE/inherit','DOT/LinearFunction','DOT/Matrix3','SCENERY/nodes/Node','SCENERY/nodes/Rectangle','KITE/Shape','SCENERY/input/SimpleDragHandler','DOT/Transform3','DOT/Vector2','image!SCENERY_PHET/../images/faucet_knob.png','image!SCENERY_PHET/../images/faucet_knob_disabled.png','image!SCENERY_PHET/../images/faucet_shaft.png','image!SCENERY_PHET/../images/faucet_flange.png','image!SCENERY_PHET/../images/faucet_flange_disabled.png','image!SCENERY_PHET/../images/faucet_stop.png','image!SCENERY_PHET/../images/faucet_pipe.png','image!SCENERY_PHET/../images/faucet_spout.png'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Circle = require( 'SCENERY/nodes/Circle' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Transform3 = require( 'DOT/Transform3' );
  var Vector2 = require( 'DOT/Vector2' );

  // images
  var knobImage = require( 'image!SCENERY_PHET/../images/faucet_knob.png' );
  var knobDisabledImage = require( 'image!SCENERY_PHET/../images/faucet_knob_disabled.png' );
  var shaftImage = require( 'image!SCENERY_PHET/../images/faucet_shaft.png' );
  var flangeImage = require( 'image!SCENERY_PHET/../images/faucet_flange.png' );
  var flangeDisabledImage = require( 'image!SCENERY_PHET/../images/faucet_flange_disabled.png' );
  var stopImage = require( 'image!SCENERY_PHET/../images/faucet_stop.png' );
  var pipeImage = require( 'image!SCENERY_PHET/../images/faucet_pipe.png' );
  var spoutImage = require( 'image!SCENERY_PHET/../images/faucet_spout.png' );

  // constants
  var DEBUG_ORIGIN = false;
  var SPOUT_OUTPUT_CENTER_X = 112; // center of spout, determined by inspecting image file
  var PIPE_Y_OFFSET = 30; // y-offset of pipe in spout image
  var SHOOTER_MIN_X_OFFSET = 4; // x-offset of shooter's off position in spout image
  var SHOOTER_MAX_X_OFFSET = 66; // x-offset of shooter's full-on position in spout image
  var SHOOTER_Y_OFFSET = 15; // y-offset of shooter's centerY in spout image
  var PIPE_X_OVERLAP = 1; // overlap between pipe and spout, so vertical seam is not visible
  var SHOOTER_WINDOW_BOUNDS = new Bounds2( 10, 10, 90, 25 ); // bounds of the window in the spout image, through which you see the shooter handle

  /**
   * @param {Faucet} faucet
   * @param {ModelViewTransform2} mvt
   * @param {*} options
   * @constructor
   */
  function FaucetNode( faucet, mvt, options ) {

    options = _.extend( {
      scale: 1,
      knobScale: 0.6 // values in the range 0.6 - 1.0 look decent
    }, options );

    var thisNode = this;
    Node.call( thisNode );

    // knob
    var knobNode = new Image( knobImage );
    var dx = 0.5 * knobNode.width;
    var dy = 0.5 * knobNode.height;
    knobNode.touchArea = Shape.rectangle( -dx, -dy, knobNode.width + dx + dx, knobNode.height + dy + dy ); // before scaling!
    knobNode.scale( options.knobScale );
    var knobDisabledNode = new Image( knobDisabledImage );
    knobDisabledNode.scale( knobNode.getScaleVector() );

    // shaft
    var shaftNode = new Image( shaftImage );

    // flange
    var flangeNode = new Image( flangeImage );
    var flangeDisabledNode = new Image( flangeDisabledImage );

    // stop
    var stopNode = new Image( stopImage );

    // assemble the shooter
    var shooterNode = new Node();
    shooterNode.addChild( shaftNode );
    shooterNode.addChild( stopNode );
    shooterNode.addChild( flangeNode );
    shooterNode.addChild( flangeDisabledNode );
    shooterNode.addChild( knobNode );
    shooterNode.addChild( knobDisabledNode );
    stopNode.x = shaftNode.x + 12;
    stopNode.centerY = shaftNode.centerY;
    flangeNode.left = shaftNode.right;
    flangeNode.centerY = shaftNode.centerY;
    flangeDisabledNode.x = flangeNode.x;
    flangeDisabledNode.y = flangeNode.y;
    knobNode.left = flangeNode.right - 8; // a bit of overlap makes this look better
    knobNode.centerY = flangeNode.centerY;
    knobDisabledNode.x = knobNode.x;
    knobDisabledNode.y = knobNode.y;

    // pipe, tiled horizontally
    var pipeNode = new Image( pipeImage, { pickable: false } );
    var pipeWidth = ( mvt.modelToViewDeltaX( faucet.location.x - faucet.pipeMinX ) / options.scale ) - SPOUT_OUTPUT_CENTER_X + PIPE_X_OVERLAP;
    assert && assert( pipeWidth > 0 );
    pipeNode.setScaleMagnitude( pipeWidth / pipeImage.width, 1 );

    // other nodes
    var spoutNode = new Image( spoutImage, { pickable: false } );
    var windowNode = new Rectangle( SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.minY,
      SHOOTER_WINDOW_BOUNDS.maxX - SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.maxY - SHOOTER_WINDOW_BOUNDS.minY,
      { fill: 'rgb(107,107,107)' } );

    // rendering order
    thisNode.addChild( windowNode );
    thisNode.addChild( shooterNode );
    thisNode.addChild( pipeNode );
    thisNode.addChild( spoutNode );

    // origin
    if ( DEBUG_ORIGIN ) {
      thisNode.addChild( new Circle( { radius: 3, fill: 'red' } ) );
    }

    // layout
    {
      // move spout's origin to the center of it's output
      spoutNode.x = -SPOUT_OUTPUT_CENTER_X;
      spoutNode.y = -spoutNode.height;
      // window is in the spout's coordinate frame
      windowNode.x = spoutNode.x;
      windowNode.y = spoutNode.y;
      // pipe connects to left edge of spout
      pipeNode.right = spoutNode.left + PIPE_X_OVERLAP;
      pipeNode.top = spoutNode.top + PIPE_Y_OFFSET;
      // shooter at top of spout
      shooterNode.left = spoutNode.left + SHOOTER_MIN_X_OFFSET;
      shooterNode.centerY = spoutNode.top + SHOOTER_Y_OFFSET;
    }

    // move to model location
    var location = mvt.modelToViewPosition( faucet.location );
    thisNode.x = location.x;
    thisNode.y = location.y;

    var offsetToFlowRate = new LinearFunction( SHOOTER_MIN_X_OFFSET, SHOOTER_MAX_X_OFFSET, 0, faucet.maxFlowRate, true /* clamp */ );

    // encourage dragging by the blue parts, but make the entire shooter draggable
    knobNode.cursor = 'pointer';
    flangeNode.cursor = 'pointer';
    shooterNode.addInputListener( new SimpleDragHandler(
      {
        target: null, // save target, because event.currentTarget is null for drag.
        startXOffset: 0, // where the drag started, relative to the target's origin, in parent view coordinates

        allowTouchSnag: true,

        start: function( event ) {
          this.target = event.currentTarget;
          this.startXOffset = this.target.globalToParentPoint( event.pointer.point ).x;
        },

        // adjust the flow
        drag: function( event ) {
          if ( faucet.enabled.get() ) {
            var xParent = this.target.globalToParentPoint( event.pointer.point ).x;
            var xOffset = xParent - this.startXOffset;
            var flowRate = offsetToFlowRate( xOffset );
            faucet.flowRate.set( flowRate );
          }
        },

        // turn off the faucet when the handle is released
        end: function() {
          faucet.flowRate.set( 0 );
          this.target = null;
        },

        // prevent default behavior that translates the node
        translate: function() {
        }
      } ) );

    faucet.flowRate.link( function( flowRate ) {
      var xOffset = offsetToFlowRate.inverse( flowRate );
      shooterNode.x = spoutNode.left + xOffset;
    } );

    faucet.enabled.link( function( enabled ) {
      knobNode.visible = enabled;
      knobDisabledNode.visible = !enabled;
      flangeNode.visible = enabled;
      flangeDisabledNode.visible = !enabled;
    } );

    thisNode.mutate( options );
  }

  inherit( Node, FaucetNode );

  return FaucetNode;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Base type for all particles.
 * Origin is at the center of the particle.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/ParticleNode',['require','PHET_CORE/inherit','SCENERY/nodes/Rectangle'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );

  /**
   * @param {SoluteParticle} particle
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function ParticleNode( particle, mvt ) {

    var thisNode = this;

    var viewSize = mvt.modelToViewDeltaX( particle.size );
    Rectangle.call( thisNode, -viewSize / 2, -viewSize / 2, viewSize, viewSize, {
      fill: particle.color,
      stroke: particle.color.darkerColor(),
      lineWidth: 1
    } );

    thisNode.particle = particle;
    thisNode.rotation = particle.orientation;

    particle.location.link( function() {
      thisNode.translation = mvt.modelToViewPosition( particle.location.get() );
    } );
  }

  inherit( Rectangle, ParticleNode );

  return ParticleNode;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Visual representation of the precipitate that forms on the bottom of the beaker when the solution is saturated.
 * Manages the creation and deletion of precipitate particle nodes.
 * Origin is at bottom center of the beaker.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/PrecipitateNode',['require','PHET_CORE/inherit','SCENERY/nodes/Node','BEERS_LAW_LAB/concentration/view/ParticleNode'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var ParticleNode = require( 'BEERS_LAW_LAB/concentration/view/ParticleNode' );

  /**
   * @param {Precipitate} precipitate
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function PrecipitateNode( precipitate, mvt ) {

    var thisNode = this;
    Node.call( thisNode, { pickable: false } );

    var particlesParent = new Node();
    thisNode.addChild( particlesParent );

    /**
     * NOTE: To optimize performance, this algorithm assumes that Precipitate
     * adds/removes particles from the end of its 'particles' array. See #48.
     */
    precipitate.registerChangedCallback( function( precipitate ) {
      var numberOfParticles =  precipitate.particles.length;
      var numberOfNodes = particlesParent.getChildrenCount();
      if ( numberOfParticles === numberOfNodes ) {
        return;
      }
      else if ( numberOfParticles === 0 ) {
        particlesParent.removeAllChildren();
      }
      else if ( numberOfParticles > numberOfNodes ) {
        // add nodes
        var index = particlesParent.getChildrenCount();
        while ( index < numberOfParticles ) {
          particlesParent.addChild( new ParticleNode( precipitate.particles[index], mvt ) );
          index++;
        }
      }
      else {
        // remove nodes
        while ( particlesParent.getChildrenCount() > numberOfParticles ) {
          particlesParent.removeChildAt( particlesParent.getChildrenCount() - 1 );
        }
      }
    } );
  }

  return inherit( Node, PrecipitateNode );
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Button that removes solute from the solution.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/RemoveSoluteButton',['require','PHET_CORE/inherit','SCENERY_PHET/PhetFont','BEERS_LAW_LAB/beers-law-lab-strings','SUN/TextButton'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var TextButton = require( 'SUN/TextButton' );

  /**
   * @param {ConcentrationSolution} solution
   * @constructor
   */
  function RemoveSoluteButton( solution ) {

    var thisButton = this;

    TextButton.call( thisButton, strings.removeSolute, function() {
      solution.soluteAmount.set( 0 );
    }, {
      font: new PhetFont( 22 ),
      textFill: 'black',
      textFillDisabled: 'rgb(175,175,175)',
      rectangleXMargin: 10,
      rectangleFillDisabled: 'white'
    } );

    // change the text fill to indicate whether the button is enabled
    solution.soluteAmount.link( function( soluteAmount ) {
      thisButton.enabled = ( soluteAmount > 0 );
    } );
  }

  inherit( TextButton, RemoveSoluteButton );

  return RemoveSoluteButton;
} );

define("image!SCENERY_PHET/../images/reset_button_up.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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WHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS4xLjIiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxMy0wNy0wOFQxMjoyODozMjwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxMy0wNy0wM1QxMTo1MjoxNDwveG1wOkNyZWF0ZURhdGU+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjcyPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj43MjwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6Q29sb3JT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';
return img;});

define("image!SCENERY_PHET/../images/reset_button_over.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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WHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS4xLjIiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxMy0wNy0wOFQxMjoyOToxNDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxMy0wNy0wM1QxMjoyNToyODwveG1wOkNyZWF0ZURhdGU+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjcyPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj43MjwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6Q29sb3JT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';
return img;});

define("image!SCENERY_PHET/../images/reset_button_down.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

define("image!SCENERY_PHET/../images/reset_button_disabled.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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WHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS4xLjIiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxMy0wNy0wOFQxMjoyOTowMjwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxMy0wNy0wM1QxMTo1NzoyOTwveG1wOkNyZWF0ZURhdGU+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjcyPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj43MjwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6Q29sb3JT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';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Reset All button.
 */
define('SCENERY_PHET/ResetAllButton',['require','SCENERY/nodes/Image','PHET_CORE/inherit','SUN/PushButton','DOT/Vector2','image!SCENERY_PHET/../images/reset_button_up.png','image!SCENERY_PHET/../images/reset_button_over.png','image!SCENERY_PHET/../images/reset_button_down.png','image!SCENERY_PHET/../images/reset_button_disabled.png'], function( require ) {
  

  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var PushButton = require( 'SUN/PushButton' );
  var Vector2 = require( 'DOT/Vector2' );
  var resetButtonUp = require( 'image!SCENERY_PHET/../images/reset_button_up.png' );
  var resetButtonOver = require( 'image!SCENERY_PHET/../images/reset_button_over.png' );
  var resetButtonDown = require( 'image!SCENERY_PHET/../images/reset_button_down.png' );
  var resetButtonDisabled = require( 'image!SCENERY_PHET/../images/reset_button_disabled.png' );

  var radius = 33; // hardcoded width/height for the circle of the reset all button
  var radiusSquared = radius * radius;
  var center = new Vector2( radius, radius );

  function ResetAllButton( callback, options ) {
    PushButton.call( this,
      new ResetAllImage( resetButtonUp ),
      new ResetAllImage( resetButtonOver ),
      new ResetAllImage( resetButtonDown ),
      new ResetAllImage( resetButtonDisabled ),
      callback, options );
  }

  function ResetAllImage( image ) {
    Image.call( this, image );
  }

  inherit( Image, ResetAllImage, {
    containsPointSelf: function( point ) {
      return point.distanceSquared( center ) <= radiusSquared;
    }
  } );

  return inherit( PushButton, ResetAllButton );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Indicator that the solution is saturated.
 * This consists of 'Saturated!' on a translucent background.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/SaturatedIndicator',['require','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY_PHET/PhetFont','SCENERY/nodes/Rectangle','BEERS_LAW_LAB/beers-law-lab-strings','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var Text = require( 'SCENERY/nodes/Text' );

  /**
   * @param {ConcentrationSolution} solution
   * @constructor
   */
  function SaturatedIndicator( solution ) {

    var thisNode = this;
    Node.call( thisNode );

    var label = new Text( strings.saturated, { font: new PhetFont( 20 ) } );

    // translucent light-gray background, so this shows up on all solution colors
    var background = new Rectangle( 0, 0, 1.2 * label.width, 1.2 * label.height, 8, 8,
      { fill: 'rgba( 240, 240, 240, 0.6 )' } );

    // rendering order
    thisNode.addChild( background );
    thisNode.addChild( label );

    // layout
    label.centerX = background.centerX;
    label.centerY = background.centerY;

    // make this node visible when the solution is saturated
    solution.concentration.link( function() {
      thisNode.setVisible( solution.isSaturated() );
    } );
  }

  inherit( Node, SaturatedIndicator );

  return SaturatedIndicator;
} );
define("image!BEERS_LAW_LAB/../images/shaker.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Shaker that contains a solute in solid form.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/ShakerNode',['require','SCENERY/nodes/Circle','SCENERY/input/DownUpListener','SCENERY/nodes/HTMLText','SCENERY/nodes/Image','PHET_CORE/inherit','BEERS_LAW_LAB/common/view/MovableDragHandler','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY_PHET/PhetFont','KITE/Shape','DOT/Vector2','image!BEERS_LAW_LAB/../images/shaker.png'], function( require ) {
  

  // imports
  var Circle = require( 'SCENERY/nodes/Circle' );
  var DownUpListener = require( 'SCENERY/input/DownUpListener' );
  var HTMLText = require( 'SCENERY/nodes/HTMLText' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var MovableDragHandler = require( 'BEERS_LAW_LAB/common/view/MovableDragHandler' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Shape = require( 'KITE/Shape' );
  var Vector2 = require( 'DOT/Vector2' );

  // images
  var shakerImage = require( 'image!BEERS_LAW_LAB/../images/shaker.png' );

  // constants
  var DEBUG_ORIGIN = false;
  var SHOW_ARROWS = true;
  var ARROW_LENGTH = 40;
  var ARROW_HEAD_LENGTH = 30;
  var ARROW_HEAD_WIDTH = 40;
  var ARROW_TAIL_WIDTH = 23;
  var ARROW_FILL = 'yellow';
  var ARROW_STROKE = 'rgb(160,160,160)';

  /**
   * Constructor
   * @param {Shaker} shaker
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function ShakerNode( shaker, mvt ) {

    var thisNode = this;
    Node.call( thisNode, { renderer: 'svg', rendererOptions: { cssTransform: true } } );

    // shaker image
    var imageNode = new Image( shakerImage );
    imageNode.setScaleMagnitude( 0.75 );

    // label
    var labelNode = new HTMLText( shaker.solute.formula, { font: new PhetFont( { size: 22, weight: 'bold' } ), fill: 'black' } );

    // arrows
    var downArrowShape = new Shape()
      .moveTo( 0, 0 )
      .lineTo( -ARROW_HEAD_WIDTH / 2, -ARROW_HEAD_LENGTH )
      .lineTo( -ARROW_TAIL_WIDTH / 2, -ARROW_HEAD_LENGTH )
      .lineTo( -ARROW_TAIL_WIDTH / 2, -ARROW_LENGTH )
      .lineTo( ARROW_TAIL_WIDTH / 2, -ARROW_LENGTH )
      .lineTo( ARROW_TAIL_WIDTH / 2, -ARROW_HEAD_LENGTH )
      .lineTo( ARROW_HEAD_WIDTH / 2, -ARROW_HEAD_LENGTH )
      .close();
    var downArrowNode = new Path( downArrowShape, {fill: ARROW_FILL, stroke: ARROW_STROKE } );
    downArrowNode.top = imageNode.bottom + 4;
    downArrowNode.centerX = imageNode.centerX;
    downArrowNode.pickable = false;
    downArrowNode.visible = false;

    var upArrowShape = new Shape()
      .moveTo( 0, 0 )
      .lineTo( -ARROW_HEAD_WIDTH / 2, ARROW_HEAD_LENGTH )
      .lineTo( -ARROW_TAIL_WIDTH / 2, ARROW_HEAD_LENGTH )
      .lineTo( -ARROW_TAIL_WIDTH / 2, ARROW_LENGTH )
      .lineTo( ARROW_TAIL_WIDTH / 2, ARROW_LENGTH )
      .lineTo( ARROW_TAIL_WIDTH / 2, ARROW_HEAD_LENGTH )
      .lineTo( ARROW_HEAD_WIDTH / 2, ARROW_HEAD_LENGTH )
      .close();
    var upArrowNode = new Path( upArrowShape, { fill: ARROW_FILL, stroke: ARROW_STROKE } );
    upArrowNode.bottom = imageNode.top - 4;
    upArrowNode.centerX = imageNode.centerX;
    upArrowNode.pickable = false;
    upArrowNode.visible = false;

    // common parent, to simplify rotation and label alignment.
    var parentNode = new Node();
    thisNode.addChild( parentNode );
    parentNode.addChild( imageNode );
    parentNode.addChild( labelNode );
    if ( SHOW_ARROWS ) {
      parentNode.addChild( upArrowNode );
      parentNode.addChild( downArrowNode );
    }
    parentNode.rotate( shaker.orientation - Math.PI ); // assumes that shaker points to the left in the image file

    // Manually adjust these values until the origin is in the middle hole of the shaker.
    parentNode.translate( -12, -imageNode.height / 2 );

    // origin
    if ( DEBUG_ORIGIN ) {
      thisNode.addChild( new Circle( { radius: 3, fill: 'red' } ) );
    }

    // sync location with model
    var shakerWasMoved = false;
    shaker.location.link( function( location ) {
      thisNode.translation = mvt.modelToViewPosition( location );
      shakerWasMoved = true;
      upArrowNode.visible = downArrowNode.visible = false;
    } );
    shakerWasMoved = false; // reset to false, because function is fired when link is performed

    // sync visibility with model
    shaker.visible.link( function( visible ) {
      thisNode.setVisible( visible );
    } );

    // sync solute with model
    shaker.solute.link( function( solute ) {
      // label the shaker with the solute formula
      labelNode.setText( solute.formula );
      // center the label on the shaker
      var capWidth = 0.3 * imageNode.width;
      labelNode.centerX = capWidth + ( imageNode.width - capWidth ) / 2;
      labelNode.centerY = imageNode.centerY;
    } );

    // interactivity
    thisNode.cursor = 'pointer';
    thisNode.addInputListener( new MovableDragHandler( shaker, mvt ) );
    thisNode.addInputListener( {
        enter: function() {
          upArrowNode.visible = downArrowNode.visible = !shakerWasMoved;
        },
        exit: function() {
          upArrowNode.visible = downArrowNode.visible = false;
        }
      }
    );
  }

  inherit( Node, ShakerNode );

  return ShakerNode;
} );

// Copyright 2002-2013, University of Colorado

/**
 * A node that can be custom-drawn with Canvas calls. Manual handling of dirty region repainting.
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define('SCENERY/nodes/CanvasNode',['require','PHET_CORE/inherit','SCENERY/scenery','SCENERY/nodes/Node','SCENERY/layers/Renderer'], function( require ) {
  
  
  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );
  
  var Node = require( 'SCENERY/nodes/Node' );
  var Renderer = require( 'SCENERY/layers/Renderer' );
  
  // pass a canvasBounds option if you want to specify the self bounds
  scenery.CanvasNode = function CanvasNode( options ) {
    Node.call( this, options );
    
    if ( options.canvasBounds ) {
      this.setCanvasBounds( options.canvasBounds );
    }
  };
  var CanvasNode = scenery.CanvasNode;
  
  inherit( Node, CanvasNode, {
    
    // how to set the bounds of the CanvasNode
    setCanvasBounds: function( selfBounds ) {
      this.invalidateSelf( selfBounds );
    },
    
    isPainted: function() {
      return true;
    },
    
    // override paintCanvas with a faster version, since fillRect and drawRect don't affect the current default path
    paintCanvas: function( wrapper ) {
      throw new Error( 'CanvasNode needs paintCanvas implemented' );
    },
    
    // override for computation of whether a point is inside the self content
    // point is considered to be in the local coordinate frame
    containsPointSelf: function( point ) {
      return false;
      // throw new Error( 'CanvasNode needs containsPointSelf implemented' );
    },
    
    // whether this node's self intersects the specified bounds, in the local coordinate frame
    // intersectsBoundsSelf: function( bounds ) {
    //   // TODO: implement?
    // },
    
    getBasicConstructor: function( propLines ) {
      return 'new scenery.CanvasNode( {' + propLines + '} )'; // TODO: no real way to do this nicely?
    }
    
  } );
  
  CanvasNode.prototype._supportedRenderers = [ Renderer.Canvas ];
  
  return CanvasNode;
} );



// Copyright 2002-2013, University of Colorado Boulder

/**
 * Manages the solid solute particles as they travel between the shaker and their inevitable demise in the beaker.
 * Rendered directly to canvas for performance.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jonathan Olson
 */
define('BEERS_LAW_LAB/concentration/view/ShakerParticlesNode',['require','DOT/Bounds2','PHET_CORE/inherit','SCENERY/nodes/CanvasNode','DOT/Vector2'], function( require ) {
  

  // imports
  var Bounds2 = require( 'DOT/Bounds2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var CanvasNode = require( 'SCENERY/nodes/CanvasNode' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * @param {ShakerParticles} shakerParticles
   * @param {ModelViewTransform2} mvt
   * @param {Bounds2} canvasBounds
   * @constructor
   */
  function ShakerParticlesNode( shakerParticles, mvt, canvasBounds ) {

    var thisNode = this;
    
    thisNode.shakerParticles = shakerParticles;
    thisNode.mvt = mvt;
    
    // Initialize with a self-bounds of activeBounds
    CanvasNode.call( thisNode, { pickable: false, canvasBounds: canvasBounds } );
    
    // if during a step we change, then trigger a repaint
    shakerParticles.registerParticleChangedCallback( function() {
      thisNode.invalidatePaint();
    } );
  }

  inherit( CanvasNode, ShakerParticlesNode, {

    // @param {CanvasContextWrapper} wrapper
    paintCanvas: function( wrapper ) {
      var context = wrapper.context;
      
      var particles = this.shakerParticles.particles;
      var halfViewSize;
      var length = particles.length;
      
      // Set and compute static properties that should be shared by all of the particles, and start the path.
      // Assumes that all particles are the same color and size.
      if ( length ) {
        wrapper.setFillStyle( particles[0].color );
        wrapper.setStrokeStyle( particles[0].color.darkerColor() );
        wrapper.setLineWidth( 1 );
        halfViewSize = this.mvt.modelToViewDeltaX( particles[0].size ) * Math.SQRT2 / 2;
        context.beginPath();
      }
      
      // draw into one big path
      for ( var i = 0; i < length; i++ ) {
        var particle = particles[i];
        
        var position = this.mvt.modelToViewPosition( particle.location.get() );
        var x = position.x;
        var y = position.y;
        var cos = Math.cos( particle.orientation ) * halfViewSize;
        var sin = Math.sin( particle.orientation ) * halfViewSize;
        context.moveTo( x + cos, y + sin );
        context.lineTo( x - sin, y + cos );
        context.lineTo( x - cos, y - sin );
        context.lineTo( x + sin, y - cos );
        context.closePath();
      }
      
      // fill and stroke the entire path
      if ( length ) {
        context.fill();
        context.stroke();
      }
    }
  } );

  return ShakerParticlesNode;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Scenery-based combo box. Composed of a button and a list of items.
 * The list of items is displayed when the button is pressed, and dismissed an item is selected
 * or the user clicks outside the list.  The list can be displayed either above or below the button.
 * <p>
 * An item in the combo box had 2 properties: {Node} node, {*} value.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/ComboBox',['require','ASSERT/assert','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY/nodes/Rectangle','KITE/Shape','DOT/Vector2'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * The button that is clicked to show the list of items.
   * @param {Node} itemNode
   * @param {*} options object with optional properties
   * @constructor
   */
  function ButtonNode( itemNode, options ) {
    var thisNode = this;
    Node.call( this );

    // up or down arrow
    var arrow = new Path( null, { fill: 'black' } );
    var arrowWidth = 0.5 * itemNode.height;
    var arrowHeight = arrowWidth * Math.sqrt( 3 ) / 2; // height of equilateral triangle
    if ( options.listPosition === 'above' ) {
      arrow.shape = new Shape().moveTo( 0, arrowHeight ).lineTo( arrowWidth / 2, 0 ).lineTo( arrowWidth, arrowHeight ).close(); // up arrow
    }
    else {
      arrow.shape = new Shape().moveTo( 0, 0 ).lineTo( arrowWidth, 0 ).lineTo( arrowWidth / 2, arrowHeight ).close(); // down arrow
    }

    // button background
    var width = itemNode.width + ( 2 * options.buttonXMargin ) + arrow.width + 5;
    var background = new Rectangle( 0, 0, width, itemNode.height + ( 2 * options.buttonYMargin ), options.buttonCornerRadius, options.buttonCornerRadius,
      { fill: options.buttonFill, stroke: options.buttonStroke, lineWidth: options.buttonLineWidth } );

    // itemNode's parent
    var itemNodeParent = new Node();

    thisNode.addChild( background );
    thisNode.addChild( arrow );
    thisNode.addChild( itemNodeParent );

    thisNode.setItemNode = function( itemNode ) {
      itemNodeParent.removeAllChildren();
      itemNodeParent.addChild( itemNode );
      itemNode.left = options.buttonXMargin;
      itemNode.top = options.buttonYMargin;
    };
    thisNode.setItemNode( itemNode );

    arrow.left = itemNode.right;
    arrow.centerY = background.centerY;
  }

  inherit( Node, ButtonNode );

  /**
   * A wrapper around the combo box item, adds margins, etc.
   * @param item
   * @param {Number} width
   * @param {Number} height
   * @param {Number} xMargin
   * @constructor
   */
  function ItemNode( item, width, height, xMargin ) {
    var thisNode = this;
    Rectangle.call( this, 0, 0, width, height );
    this.item = item;
    thisNode.addChild( item.node );
    item.node.x = xMargin;
    item.node.centerY = height / 2;
  }

  inherit( Rectangle, ItemNode );

  /**
   * @param {Array} items
   * @param {*} property
   * @param {Node} listParent node that will be used as the list's parent, use this to ensuring that the list is in front of everything else
   * @param {*} options object with optional properties
   */
  function ComboBox( items, property, listParent, options ) {

    var thisNode = this;

    options = _.extend( {
        labelNode: null, // optional label, placed to the left of the combo box
        labelXSpacing: 10, // horizontal space between label and combo box
        // button
        buttonFill: 'white',
        buttonStroke: 'black',
        buttonLineWidth: 1,
        buttonCornerRadius: 3,
        buttonXMargin: 4,
        buttonYMargin: 4,
        // list
        listPosition: 'below', // where the list is positioned relative to the button, either 'below' or 'above'
        listXMargin: 4,
        listYMargin: 4,
        listFill: 'white',
        listStroke: 'black',
        listLineWidth: 1,
        listCornerRadius: 5,
        // items
        itemXMargin: 6,
        itemYMargin: 6,
        itemHighlightFill: 'rgb(245,245,245)',
        itemHighlightStroke: null,
        itemHighlightLineWidth: 1
      },
      options );

    Node.call( thisNode, options );

    // optional label
    if ( options.labelNode !== null ) {
      thisNode.addChild( options.labelNode );
    }

    // determine uniform dimensions for button and list items
    var itemWidth = 0, itemHeight = 0;
    for ( var i = 0; i < items.length; i++ ) {
      var item = items[i];
      if ( item.node.width > itemWidth ) { itemWidth = item.node.width; }
      if ( item.node.height > itemHeight ) { itemHeight = item.node.height; }
    }
    itemWidth += ( 2 * options.itemXMargin );
    itemHeight += ( 2 * options.itemYMargin );

    // button, will be set to correct value when property observer is registered
    var buttonNode = new ButtonNode( new ItemNode( items[0], itemWidth, itemHeight, options.itemXMargin ), options );
    thisNode.addChild( buttonNode );

    // list
    var listWidth = itemWidth + ( 2 * options.listXMargin );
    var listHeight = ( items.length * itemHeight ) + ( 2 * options.listYMargin );
    var listNode = new Rectangle( 0, 0, listWidth, listHeight, options.listCornerRadius, options.listCornerRadius,
      { fill: options.listFill, stroke: options.listStroke, lineWidth: options.listLineWidth, visible: false } );
    listParent.addChild( listNode );

    //TODO move these to ItemNode
    // how to highlight an item in the list
    var highlightItem = function( itemNode ) {
      itemNode.fill = options.itemHighlightFill;
      itemNode.stroke = options.itemHighlightStroke;
    };
    var unhighlightItem = function( itemNode ) {
      itemNode.fill = null;
      itemNode.stroke = null;
    };

    // listener that we'll attach to each item in the list
    var itemListener = {
      enter: function( event ) {
        highlightItem( event.currentTarget );
      },
      exit: function( event ) {
        unhighlightItem( event.currentTarget );
      },
      down: function( event ) {
        event.abort(); // prevent click-to-dismiss on the list
      },
      up: function( event ) {
        unhighlightItem( event.currentTarget );
        property.value = event.currentTarget.item.value; // set the property
        listNode.visible = false; // close the list
        thisNode.getUniqueTrail().rootNode().removeInputListener( clickToDismissListener ); // remove the click-to-dismiss listener
        event.abort(); // prevent nodes (eg, controls) behind the list from receiving the event
      }
    };

    // populate list with items
    for ( var j = 0; j < items.length; j++ ) {
      // add item to list
      var itemNode = new ItemNode( items[j], itemWidth, itemHeight, options.itemXMargin );
      listNode.addChild( itemNode );
      itemNode.left = options.listXMargin;
      itemNode.top = options.listYMargin + ( j * itemHeight );

      // item interactivity
      itemNode.cursor = 'pointer';
      itemNode.addInputListener( itemListener );
    }

    //TODO handle scale and rotation
    // Handles the coordinate transform required to make the list pop up near the button.
    var moveList = function() {
      var pButtonGlobal, pButtonLocal;
      if ( options.listPosition === 'above' ) {
        pButtonGlobal = thisNode.localToGlobalPoint( new Vector2( buttonNode.left, buttonNode.top ) );
        pButtonLocal = listParent.globalToLocalPoint( pButtonGlobal );
        listNode.left = pButtonLocal.x;
        listNode.bottom = pButtonLocal.y;
      }
      else {
        pButtonGlobal = thisNode.localToGlobalPoint( new Vector2( buttonNode.left, buttonNode.bottom ) );
        pButtonLocal = listParent.globalToLocalPoint( pButtonGlobal );
        listNode.left = pButtonLocal.x;
        listNode.top = pButtonLocal.y;
      }
    };

    /**
     * Because clickToDismissListener is added to the scene, it receives the 'down' event that
     * buttonNode received to register the listener. This is because scenery propagates events
     * up the event trail, and the scene is further up the trail than the button.  This flag
     * is used to ignore the first 'down' event, which is the one that the button received.
     * If we don't do this, then we never see the list because it is immediately popped down.
     * This behavior is may change, and is being discussed in scenery#58.
     */
    var enableClickToDismissListener;
    var sceneNode; // store the node that clickToDismissListener is added to, because the scene may change, see sun#14

    // listener for 'click outside to dismiss'
    var clickToDismissListener = {
      down: function( event ) {
        if ( enableClickToDismissListener ) {
          sceneNode.removeInputListener( clickToDismissListener );
          listNode.visible = false;
        }
        else {
          enableClickToDismissListener = true;
        }
      }
    };

    // button interactivity
    buttonNode.cursor = 'pointer';
    buttonNode.addInputListener(
      {
        down: function( event ) {
          if ( !listNode.visible ) {
            moveList();
            listNode.visible = true;
            enableClickToDismissListener = false;
            sceneNode = thisNode.getUniqueTrail().rootNode();
            sceneNode.addInputListener( clickToDismissListener );
          }
        }
      } );

    // layout
    if ( options.labelNode ) {
      buttonNode.left = options.labelNode.right + options.labelXSpacing;
      buttonNode.centerY = options.labelNode.centerY;
    }

    // when property changes, update button
    property.link( function( value ) {
      // TODO brute force search, better way?
      var item = null;
      for ( var i = 0; i < items.length; i++ ) {
        if ( items[i].value === value ) {
          item = items[i];
        }
      }
      assert && assert( item !== null );
      buttonNode.setItemNode( new ItemNode( item, itemWidth, itemHeight, options.itemXMargin ) );
    } );
  }

  inherit( Node, ComboBox );

  /**
   * Creates a combo box item.
   * This exists primarily to document the structure of an item.
   * @param {Node} node
   * @param {*} value
   * @returns {{node: *, value: *}}
   */
  ComboBox.createItem = function( node, value ) {
    return { node: node, value: value };
  };

  return ComboBox;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Combo box for choosing a solute.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/SoluteComboBox',['require','ASSERT/assert','SUN/ComboBox','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY_PHET/PhetFont','SCENERY/nodes/Rectangle','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var ComboBox = require( 'SUN/ComboBox' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );

  /**
   * @param {Array<Solute>} solutes
   * @param {Property<Solute>} selectedSolute
   * @param {Node} soluteListParent
   * @constructor
   */
  function SoluteComboBox( solutes, selectedSolute, soluteListParent ) {

    // 'Solute' label
    var labelNode = new Text( StringUtils.format( strings.pattern_0label, strings.solute ),
      { font: new PhetFont( 22 ) } );

    // items
    var items = [];
    for ( var i = 0; i < solutes.length; i++ ) {
      var solute = solutes[i];
      items[i] = createItem( solute );
    }

    ComboBox.call( this, items, selectedSolute, soluteListParent, {
      labelNode: labelNode,
      listPosition: 'below',
      itemYMargin: 12,
      itemHighlightFill: 'rgb(218,255,255)'
    } );
  }

  inherit( ComboBox, SoluteComboBox );

  /**
   * Creates an item for the combo box.
   * @param solute
   * @returns {*|{node: *, value: *}}
   */
  var createItem = function( solute ) {
    var node = new Node();
    var colorNode = new Rectangle( 0, 0, 20, 20, { fill: solute.colorScheme.maxColor, stroke: solute.colorScheme.maxColor.darkerColor() } );
    var textNode = new Text( solute.name, { font: new PhetFont( 20 ) } );
    node.addChild( colorNode );
    node.addChild( textNode );
    textNode.left = colorNode.right + 5;
    textNode.centerY = colorNode.centerY;
    return ComboBox.createItem( node, solute );
  };

  return SoluteComboBox;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Base class for radio buttons.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/RadioButton',['require','SCENERY/input/ButtonListener','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Path','KITE/Shape'], function( require ) {
  

  // imports
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );

  /**
   * @param property
   * @param value the value that corresponds to this button, same type as property
   * @param {Node} selectedNode node that will be displayed when the button is selected
   * @param {Node} deselectedNode node that will be displayed when the button is deselected
   * @param {object} options
   * @constructor
   */
  function RadioButton( property, value, selectedNode, deselectedNode, options ) {

    options = _.extend( {
      cursor: 'pointer'
    }, options );

    var thisNode = this;
    Node.call( thisNode, options );

    var background = new Path( Shape.bounds( selectedNode.bounds.union( deselectedNode.bounds ) ) );

    thisNode.addChild( background );
    thisNode.addChild( selectedNode );
    thisNode.addChild( deselectedNode );

    // sync control with model
    property.link( function( newValue ) {
      selectedNode.visible = ( newValue === value );
      deselectedNode.visible = !selectedNode.visible;
    } );

    // set property value on fire
    thisNode.addInputListener( new ButtonListener( {
      fire: function() {
        property.set( value );
      }
    } ) );
  }

  inherit( Node, RadioButton );

  return RadioButton;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Radio button with a pseudo-Aqua (Mac OS) look. See "options" comment for list of options.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SUN/AquaRadioButton',['require','SCENERY/nodes/Circle','PHET_CORE/inherit','SCENERY/nodes/Node','SUN/RadioButton'], function( require ) {
  

  // imports
  var Circle = require( 'SCENERY/nodes/Circle' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var RadioButton = require( 'SUN/RadioButton' );

  /**
   * @param property
   * @param value the value that corresponds to this button, same type as property
   * @param {Node} node that will be vertically centered to the right of the button
   * @param {object} options
   * @constructor
   */
  function AquaRadioButton( property, value, node, options ) {

    options = _.extend( {
      cursor: 'pointer',
      selectedColor: 'rgb( 143, 197, 250 )', // color used to fill the button when it's selected
      deselectedColor: 'white', // color used to fill the button when it's deselected
      centerColor: 'black', // color used to fill the center of teh button when it's selected
      radius: 16, // radius of the button
      xSpacing: 8, // horizontal space between the button and the node
      stroke: 'black' // color used to stroke the outer edge of the button
    }, options );

    // selected node
    var selectedNode = new Node();
    var innerCircle = new Circle( options.radius / 3, { fill: options.centerColor } );
    var outerCircleSelected = new Circle( options.radius, { fill: options.selectedColor, stroke: options.stroke } );
    selectedNode.addChild( outerCircleSelected );
    selectedNode.addChild( innerCircle );
    selectedNode.addChild( node );
    node.left = outerCircleSelected.right + options.xSpacing;
    node.centerY = outerCircleSelected.centerY;

    // deselected node
    var deselectedNode = new Node();
    var outerCircleDeselected = new Circle( options.radius, { fill: options.deselectedColor, stroke: options.stroke } );
    deselectedNode.addChild( outerCircleDeselected );
    deselectedNode.addChild( node );
    node.left = outerCircleDeselected.right + options.xSpacing;
    node.centerY = outerCircleDeselected.centerY;

    RadioButton.call( this, property, value, selectedNode, deselectedNode, options );
  }

  inherit( RadioButton, AquaRadioButton );

  return AquaRadioButton;
} );
define("image!BEERS_LAW_LAB/../images/shaker-icon.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

define("image!BEERS_LAW_LAB/../images/dropper-icon.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAyCAYAAABcfPsmAAAABGdBTUEAANbY1E9YMgAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAU4SURBVHja7FdbbBRVGP7PmTMze+8u2zuUEqSFLfUSrQH1BRPwySBE4wNGSZrgky/GiCYkPBpIVAwGQkw0JD5gjBjeRIIESWmiqKkVhFBgtU1LodvuTndnd2fnnBn/c2a7XIQYEx809iQnOztnzne+//sv8w/59pNXYWFolMDJ78fh2OmrMJ2z413t8WczK5Zs6miOdlMcM/nK5MXs7OnfrlvHhPBzA31tMLi5H1YubQKXewqDwW2DEqIWElFj24sbe/c8PdDVlU5GwNA1YBrBJwgU7dpLw6OTew4d/Xl31REH4a7RACQIVnE4cO7tffPlgZ09y1Mg8FDfx+n54OE6pQBNcRO2buhJD/S1H/j42OhjVYfvwAO9BiAh9Qtk8N35m2+sf7BzZ3dHAhzXg5ChKXY6ozgJaMwA4nvAhYClrXHYsfXhwa+HszOU5t5uAHpeoN3UjN0dixi7lrfHlNmmwZARVQeZpgGhZBfQcBp8UQNh3wC3NKNAe7pTr3/5zdhniZgxIq1hVyYKuFEClp4fyLSlUGxgyEgyJ8pRAKGmTtDTGaBGQgFSPQrAq1AtF+CR3lbjq7PZbdcmrRFpCfvgyE+AOgA64Mln1neDBNTwKKWdmvIaoSnKrZlKa6IZ4FNNaRsJ6dCSijxx9NQYRMM6MLvsQrnKIRkPtUovC/CVFh4CeUovArwyB6yaR8oaLrggHAunjevyQQ/am6PtyM5E6RwqzdU05UHiw32G33B3fYrgF0XxVbgBhUAhdVHfBH9jkDs2SVUasQz/8Pj3A6rU45hjwvc9GeCedNLC1GRg42SYLbqO8Yfh4qF+jIHA+xynjAwpIZfFwcQlmRXNyfAmRukD56/OYhT4CiQAIyrITaMAeqwMRA8rD/u1IvDyLMaskEUAKlXR0tESe27OqnxOnnp07WtvbR/4sC0dDYJaMQqY6WyBoa4mRWaE1CMEw8YTDgjuKmvmrCrsOjj0Dtu0bvkg1j0oV3gdiCgQA800sTiETAbReAr0xFKVemi3Cm7PmQduT0PNzmMhEdDZGoMXNvZuZ5btWKSeuB5GPYcg3eS1KzhUay7YZQfYbA4PRIYyA1SCCGQosMT5ygeca3Bzzi6y48O/7+5sjh3tW5luEQgidZGAJCiSjRCWQUzqdTMoeURVI03T1D0sDvaR45d2kRVd3dIRzfGIsYpzN7V5y5ZXVmcyZQmgo1cNw4BoNAqJeBwobnZdF0olmceef/HChfinhw9/gawn7Io7judMMSkouj5Xrro5x3FS6bbODavWrLUkyXAoBKFwGJqamqA5nVYmO04V8oUCWuL5xVIlVSrXfkCWWY0GJY/VLVO0ZaFAXRh3XUOa7dbDx605UEMgTzD8dcDFifL4nHMd14nct1D5/4e5vAi4CLgIuAi4CPjfBST36VnVC77+diO37v0lIA2Gp5pc1esGbYn82OHYmuD7WHVoCObjuxg/GHxyz0+z+on54aGh6/39/cuYbswUi0WtXKlAcb4IN25MY0NAgzaFUtlOJk+eOFHAwyZkA9D4AF2STN76g63GxPj4SDZ7rW9NX2ZZJBKZr9UcKvsdbFOgVquBT4iwrELLoQMH7aEzZ/aZplm6ndQdgIoyY+71yamzv4yOtvas7s2k0+l53/OIbIxMMyRyuVzH/vf3XRkdGXkvHA6X7pbsT4AKVGdQyOdHLv16Mfn4unW9rW1tlUQiQZBt6t09ey+PXb68H9mLeznvnoB1pmBZ1tjsbO4hq1AIZa9lyelTp/iP5859hMwq9wubPwQYAIJHYUOu1cMMAAAAAElFTkSuQmCC';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Radio buttons that select the solution form, either solid (shaker) or stock solution (dropper).
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/SoluteFormNode',['require','SUN/AquaRadioButton','SCENERY/nodes/Image','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY_PHET/PhetFont','AXON/Property','KITE/Shape','BEERS_LAW_LAB/beers-law-lab-strings','SCENERY/nodes/Text','image!BEERS_LAW_LAB/../images/shaker-icon.png','image!BEERS_LAW_LAB/../images/dropper-icon.png'], function( require ) {
  

  // imports
  var AquaRadioButton = require( 'SUN/AquaRadioButton' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var Text = require( 'SCENERY/nodes/Text' );

  // images
  var shakerIconImage = require( 'image!BEERS_LAW_LAB/../images/shaker-icon.png' );
  var dropperIconImage = require( 'image!BEERS_LAW_LAB/../images/dropper-icon.png' );

  /**
   * @param {String} text
   * @param {*} textOptions
   * @param {*} image any type supported by scenery.Image
   * @param {Number} xSpacing
   * @constructor
   */
  function TextAndIconNode( text, textOptions, image, xSpacing ) {
    Node.call( this );
    var textNode = new Text( text, textOptions );
    var imageNode = new Image( image );
    this.addChild( textNode );
    this.addChild( imageNode );
    imageNode.left = textNode.right + xSpacing;
    imageNode.centerY = textNode.centerY;
  }

  inherit( Node, TextAndIconNode );

  /**
   * @param {Shaker} shaker
   * @param {Dropper} dropper
   * @constructor
   */
  function SoluteFormNode( shaker, dropper ) {

    Node.call( this );

    var TEXT_OPTIONS = { font: new PhetFont( 22 ), fill: 'black' };
    var X_SPACING = 10;
    var shakerButton = new AquaRadioButton( shaker.visible, true, new TextAndIconNode( strings.solid, TEXT_OPTIONS, shakerIconImage, X_SPACING ) );
    var dropperButton = new AquaRadioButton( dropper.visible, true, new TextAndIconNode( strings.solution, TEXT_OPTIONS, dropperIconImage, X_SPACING ) );
    var separator = new Path( Shape.lineSegment( 0, 0, 0, shakerButton.height ), { stroke: 'rgb(150,150,150)', lineWidth: 0.5 } );

    // rendering order
    this.addChild( shakerButton );
    this.addChild( separator );
    this.addChild( dropperButton );

    // layout
    var SEPARATOR_SPACING = 30;
    separator.left = shakerButton.right + SEPARATOR_SPACING;
    separator.centerY = shakerButton.centerY;
    dropperButton.left = separator.right + SEPARATOR_SPACING;

    // ensure mutual exclusivity
    shaker.visible.link( function( visible ) {
      dropper.visible.set( !visible );
    } );
    dropper.visible.link( function( visible ) {
      shaker.visible.set( !visible );
    } );
  }

  inherit( Node, SoluteFormNode );

  return SoluteFormNode;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Control panel for selecting the solute and changing its form (solid or solution).
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/SoluteControls',['require','PHET_CORE/inherit','SCENERY/nodes/Node','SUN/Panel','BEERS_LAW_LAB/concentration/view/SoluteComboBox','BEERS_LAW_LAB/concentration/view/SoluteFormNode'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Panel = require( 'SUN/Panel' );
  var SoluteComboBox = require( 'BEERS_LAW_LAB/concentration/view/SoluteComboBox' );
  var SoluteFormNode = require( 'BEERS_LAW_LAB/concentration/view/SoluteFormNode' );

  /**
   * @param {Array<Solute>} solutes
   * @param {Property<Solute>} currentSolute
   * @param {Shaker} shaker
   * @param {Dropper} dropper
   * @param {Node} soluteListParent
   * @constructor
   */
  function SoluteControlsNode( solutes, currentSolute, shaker, dropper, soluteListParent ) {

    // solute combo box
    var soluteComboBox = new SoluteComboBox( solutes, currentSolute, soluteListParent );

    // radio buttons for solid vs solution
    var soluteFormNode = new SoluteFormNode( shaker, dropper );

    var contentNode = new Node();
    contentNode.addChild( soluteFormNode );
    contentNode.addChild( soluteComboBox ); // add last, so that dropdown is on top

    // layout
    soluteFormNode.left = soluteComboBox.left;
    soluteFormNode.top = soluteComboBox.bottom + 15;

    Panel.call( this, contentNode,
      { xMargin: 15, yMargin: 15, fill: '#F0F0F0', stroke: 'gray', lineWidth: 1 } );
  }

  inherit( Panel, SoluteControlsNode );

  return SoluteControlsNode;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Solution that appears in the beaker.
 * Origin is at bottom center of beaker.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/SolutionNode',['require','PHET_CORE/inherit','SCENERY/nodes/Rectangle','DOT/Util'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Util = require( 'DOT/Util' );

  // constants
  var MIN_NONZERO_HEIGHT = 5; // minimum height for a solution with non-zero volume, set by visual inspection

  /**
   * @param {ConcentrationSolution} solution
   * @param {Beaker} beaker
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function SolutionNode( solution, beaker, mvt ) {

    var thisNode = this;
    Rectangle.call( thisNode, 0, 0, 1, 1, { lineWidth: 1 } );

    thisNode.solution = solution;
    thisNode.beaker = beaker;

    /*
     * Updates the color of the solution, accounting for saturation.
     * @param {Color} color
     */
    solution.color.link( function( color ) {
      thisNode.fill = color;
      thisNode.stroke = color.darkerColor();
    } );

    /*
     * Updates the amount of stuff in the beaker, based on solution volume.
     * @param {Number} volume
     */
    var viewLocation = mvt.modelToViewPosition( beaker.location );
    var viewWidth = mvt.modelToViewDeltaX( beaker.size.width );
    solution.volume.link( function( volume ) {

      // determine dimensions in model coordinates
      var solutionHeight = Util.linear( 0, beaker.volume, 0, beaker.size.height, volume ); // volume -> height
      if ( volume > 0 && solutionHeight < MIN_NONZERO_HEIGHT ) {
        // constrain non-zero volume to minimum height, so that the solution is visible to the user and detectable by the concentration probe
        solutionHeight = MIN_NONZERO_HEIGHT;
      }

      // convert to view coordinates and create shape
      var viewHeight = mvt.modelToViewDeltaY( solutionHeight );
      thisNode.setRect( viewLocation.x - (viewWidth / 2), viewLocation.y - viewHeight, viewWidth, viewHeight );
    } );
  }

  inherit( Rectangle, SolutionNode );

  return SolutionNode;

} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Stock solution coming out of the dropper.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/StockSolutionNode',['require','BEERS_LAW_LAB/concentration/model/ConcentrationSolution','BEERS_LAW_LAB/concentration/view/DropperNode','PHET_CORE/inherit','SCENERY/nodes/Path','SCENERY/nodes/Rectangle'], function( require ) {
  

  // imports
  var ConcentrationSolution = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationSolution' );
  var DropperNode = require( 'BEERS_LAW_LAB/concentration/view/DropperNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );

  /**
   * @param {Solvent} solvent
   * @param {Property<Solute>} solute
   * @param {Dropper} dropper
   * @param {Beaker} beaker
   * @param {Number} tipWidth
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function StockSolutionNode( solvent, solute, dropper, beaker, tipWidth, mvt ) {

    var thisNode = this;

    Rectangle.call( thisNode, 0, 0, 0, 0, { lineWidth: 1 } );

    // shape and location
    var updateShapeAndLocation = function() {
      // path
      if ( dropper.on.get() && !dropper.empty.get() ) {
        thisNode.setRect( -tipWidth / 2, 0, tipWidth, beaker.location.y - dropper.location.get().y );
      }
      else {
        thisNode.setRect( 0, 0, 0, 0 );
      }
      // move this node to the dropper's location
      thisNode.translation = mvt.modelToViewPosition( dropper.location.get() );
    };
    dropper.location.link( updateShapeAndLocation );
    dropper.on.link( updateShapeAndLocation );
    dropper.empty.link( updateShapeAndLocation );

    // set color to match solute
    solute.link( function( solute ) {
      var color = ConcentrationSolution.createColor( solvent, solute, solute.stockSolutionConcentration );
      thisNode.fill = color;
      thisNode.stroke = color.darkerColor();
    } );

    // hide this node when the dropper is invisible
    dropper.visible.link( function( visible ) {
      thisNode.setVisible( visible );
    } );
  }

  inherit( Rectangle, StockSolutionNode );

  return StockSolutionNode;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * View for the 'Concentration' screen.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/view/ConcentrationView',['require','BEERS_LAW_LAB/concentration/view/BeakerNode','DOT/Bounds2','BEERS_LAW_LAB/concentration/view/ConcentrationMeterNode','SCENERY/nodes/DOM','DOT/Dimension2','BEERS_LAW_LAB/concentration/view/DropperNode','BEERS_LAW_LAB/concentration/view/EvaporationControl','BEERS_LAW_LAB/concentration/view/FaucetFluidNode','SCENERY_PHET/FaucetNode','PHET_CORE/inherit','SCENERY/nodes/Node','BEERS_LAW_LAB/concentration/view/PrecipitateNode','DOT/Range','BEERS_LAW_LAB/concentration/view/RemoveSoluteButton','SCENERY_PHET/ResetAllButton','BEERS_LAW_LAB/concentration/view/SaturatedIndicator','SCENERY/Scene','JOIST/ScreenView','BEERS_LAW_LAB/concentration/view/ShakerNode','BEERS_LAW_LAB/concentration/view/ShakerParticlesNode','BEERS_LAW_LAB/concentration/model/Solute','BEERS_LAW_LAB/concentration/view/SoluteControls','BEERS_LAW_LAB/concentration/view/SolutionNode','BEERS_LAW_LAB/concentration/view/StockSolutionNode'], function( require ) {
  

  // imports
  var BeakerNode = require( 'BEERS_LAW_LAB/concentration/view/BeakerNode' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var ConcentrationMeterNode = require( 'BEERS_LAW_LAB/concentration/view/ConcentrationMeterNode' );
  var DOM = require( 'SCENERY/nodes/DOM' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var DropperNode = require( 'BEERS_LAW_LAB/concentration/view/DropperNode' );
  var EvaporationControl = require( 'BEERS_LAW_LAB/concentration/view/EvaporationControl' );
  var FaucetFluidNode = require( 'BEERS_LAW_LAB/concentration/view/FaucetFluidNode' );
  var FaucetNode = require( 'SCENERY_PHET/FaucetNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var PrecipitateNode = require( 'BEERS_LAW_LAB/concentration/view/PrecipitateNode' );
  var Range = require( 'DOT/Range' );
  var RemoveSoluteButton = require( 'BEERS_LAW_LAB/concentration/view/RemoveSoluteButton' );
  var ResetAllButton = require( 'SCENERY_PHET/ResetAllButton' );
  var SaturatedIndicator = require( 'BEERS_LAW_LAB/concentration/view/SaturatedIndicator' );
  var Scene = require( 'SCENERY/Scene' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var ShakerNode = require( 'BEERS_LAW_LAB/concentration/view/ShakerNode' );
  var ShakerParticlesNode = require( 'BEERS_LAW_LAB/concentration/view/ShakerParticlesNode' );
  var Solute = require( 'BEERS_LAW_LAB/concentration/model/Solute' );
  var SoluteControls = require( 'BEERS_LAW_LAB/concentration/view/SoluteControls' );
  var SolutionNode = require( 'BEERS_LAW_LAB/concentration/view/SolutionNode' );
  var StockSolutionNode = require( 'BEERS_LAW_LAB/concentration/view/StockSolutionNode' );

  /**
   * @param {ConcentrationModel} model
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function ConcentrationView( model, mvt ) {

    var thisView = this;
    ScreenView.call( thisView, { renderer: 'svg' } );

    // Beaker and stuff inside it
    var beakerNode = new BeakerNode( model.beaker, mvt );
    var solutionNode = new SolutionNode( model.solution, model.beaker, mvt );
    var precipitateNode = new PrecipitateNode( model.precipitate, mvt );
    var saturatedIndicator = new SaturatedIndicator( model.solution );

    // Shaker
    var shakerNode = new ShakerNode( model.shaker, mvt );

    // Shaker particles are drawn using canvas. Specify bounds of the canvas (smaller for speed).
    var shakerParticlesBounds = new Bounds2(
      mvt.modelToViewX( model.beaker.getLeft() ), thisView.layoutBounds.minY,
      mvt.modelToViewX( model.beaker.getRight() ), mvt.modelToViewY( model.beaker.location.y ) );
    var shakerParticlesNode = new ShakerParticlesNode( model.shakerParticles, mvt, shakerParticlesBounds );

    // Dropper
    var dropperNode = new DropperNode( model.dropper, model.solution.solvent, model.solution.solute, mvt );
    var stockSolutionNode = new StockSolutionNode( model.solution.solvent, model.solute, model.dropper, model.beaker, dropperNode.getTipWidth(), mvt );

    // faucets
    var solventFaucetNode = new FaucetNode( model.solventFaucet, mvt, { scale: 0.75 } );
    var drainFaucetNode = new FaucetNode( model.drainFaucet, mvt, { scale: 0.75 } );
    var SOLVENT_FLUID_HEIGHT = model.beaker.location.y - model.solventFaucet.location.y;
    var DRAIN_FLUID_HEIGHT = 1000; // tall enough that resizing the play area is unlikely to show bottom of fluid
    var solventFluidNode = new FaucetFluidNode( model.solventFaucet, model.solution.solvent, SOLVENT_FLUID_HEIGHT, mvt );
    var drainFluidNode = new FaucetFluidNode( model.drainFaucet, model.solution, DRAIN_FLUID_HEIGHT, mvt );

    // Concentration meter
    var concentrationMeterNode = new ConcentrationMeterNode( model.concentrationMeter, model.solution, model.dropper,
      solutionNode, stockSolutionNode, solventFluidNode, drainFluidNode, mvt );

    // Solute controls
    var soluteListParent = new Node();
    var soluteControls = new SoluteControls( model.solutes, model.solute, model.shaker, model.dropper, soluteListParent );

    // Evaporation control
    var evaporationControl = new EvaporationControl( model.evaporator );

    // Remove Solute button
    var removeSoluteButton = new RemoveSoluteButton( model.solution );

    // Reset All button
    var resetAllButton = new ResetAllButton( function() {
      model.reset();
    } );

    // Rendering order
    thisView.addChild( solventFluidNode );
    thisView.addChild( solventFaucetNode );
    thisView.addChild( drainFluidNode );
    thisView.addChild( drainFaucetNode );
    thisView.addChild( stockSolutionNode );
    thisView.addChild( solutionNode );
    thisView.addChild( beakerNode.mutate( { layerSplit: true } ) ); // beaker is static, put in its own layer
    thisView.addChild( precipitateNode );
    thisView.addChild( saturatedIndicator );
    thisView.addChild( shakerParticlesNode );
    thisView.addChild( shakerNode );
    thisView.addChild( dropperNode );
    thisView.addChild( evaporationControl );
    thisView.addChild( removeSoluteButton );
    thisView.addChild( resetAllButton );
    thisView.addChild( soluteControls );
    thisView.addChild( concentrationMeterNode );
    thisView.addChild( soluteListParent ); // last, so that combo box list is on top

    // Layout for things that don't have a location in the model.
    {
      // centered towards bottom of beaker
      var saturatedIndicatorVisible = saturatedIndicator.visible; // so we can layout an invisible node
      saturatedIndicator.visible = true;
      saturatedIndicator.centerX = beakerNode.centerX;
      saturatedIndicator.bottom = beakerNode.bottom - 30;
      saturatedIndicator.visible = saturatedIndicatorVisible;
      // upper right
      soluteControls.right = concentrationMeterNode.right + 100;
      soluteControls.top = 20;
      // left-aligned below beaker
      evaporationControl.left = mvt.modelToViewPosition( model.beaker.location ).x - mvt.modelToViewDeltaX( model.beaker.size.width / 2 );
      evaporationControl.top = beakerNode.bottom + 30;
      // left of evaporation control
      removeSoluteButton.left = evaporationControl.right + 30;
      removeSoluteButton.centerY = evaporationControl.centerY;
      // lower right
      resetAllButton.left = drainFaucetNode.right + 100;
      resetAllButton.centerY = removeSoluteButton.centerY;
    }
  }

  inherit( ScreenView, ConcentrationView, { layoutBounds: new Bounds2( 0, 0, 1100, 700 ) } );

  return ConcentrationView;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Transform between model and view coordinate frames,
 * and provides convenience methods over an actual dot.Transform3
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 * @author Sam Reid
 */
define('PHETCOMMON/view/ModelViewTransform2',['require','PHET_CORE/inherit','DOT/Vector2','DOT/Matrix3','DOT/Transform3'], function ( require ) {
  

  var inherit = require( 'PHET_CORE/inherit' );
  
  var Vector2 = require( 'DOT/Vector2' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var Transform3 = require( 'DOT/Transform3' );
  
  function ModelViewTransform2( matrix ) {
    Transform3.call( this, matrix );
  }
  
  inherit( Transform3, ModelViewTransform2, {
    // convenience model => view
    modelToViewPosition: function( point )  { return this.transformPosition2( point ); },
    modelToViewX:        function( x )      { return this.matrix.m00() * x + this.matrix.m02();},
    modelToViewY:        function( y )      { return this.matrix.m11() * y + this.matrix.m12();},
    modelToViewDelta:    function( vector ) { return this.transformDelta2( vector ); },
    modelToViewNormal:   function( normal ) { return this.transformNormal2( normal ); },
    modelToViewDeltaX:   function( x )      { return this.transformDeltaX( x ); },
    modelToViewDeltaY:   function( y )      { return this.transformDeltaY( y ); },
    modelToViewBounds:   function( bounds ) { return this.transformBounds2( bounds ); },
    modelToViewShape:    function( shape )  { return this.transformShape( shape ); },
    modelToViewRay:      function( ray )    { return this.transformRay2( ray ); },
    
    // convenience view => model
    viewToModelPosition: function( point )  { return this.inversePosition2( point ); },
    viewToModelX:        function( x )      { return this.inversePosition2( new Vector2( x, 0 ) ).x; },
    viewToModelY:        function( y )      { return this.inversePosition2( new Vector2( 0, y ) ).y; },
    viewToModelDelta:    function( vector ) { return this.inverseDelta2( vector ); },
    viewToModelNormal:   function( normal ) { return this.inverseNormal2( normal ); },
    viewToModelDeltaX:   function( x )      { return this.inverseDeltaX( x ); },
    viewToModelDeltaY:   function( y )      { return this.inverseDeltaY( y ); },
    viewToModelBounds:   function( bounds ) { return this.inverseBounds2( bounds ); },
    viewToModelShape:    function( shape )  { return this.inverseShape( shape ); },
    viewToModelRay:      function( ray )    { return this.inverseRay2( ray ); }
  } );
  
  /*---------------------------------------------------------------------------*
  * Factory methods
  *----------------------------------------------------------------------------*/

  /**
   * Creates a ModelViewTransform that uses the identity transform (i.e. model coordinates are the same as view coordinates)
   *
   * @return the identity ModelViewTransform
   */
  ModelViewTransform2.createIdentity = function() {
    return new ModelViewTransform2( Matrix3.IDENTITY );
  };

  /**
   * Creates a ModelViewTransform that has the specified scale and offset such that
   * view = model * scale + offset
   *
   * @param offset {Vector2} the offset in view coordinates
   * @param scale  {Number} the scale to map model to view
   * @return the resultant ModelViewTransform2
   */
  ModelViewTransform2.createOffsetScaleMapping = function( offset, scale ) {
    return new ModelViewTransform2( Matrix3.affine( scale, 0, 0, scale, offset.x, offset.y ) );
  };

  /**
   * Creates a shearless ModelViewTransform that has the specified scale and offset such that
   * view.x = model.x * xScale + offset.x
   * view.y = model.y * yScale + offset.y
   *
   * @param offset {Vector2} the offset in view coordinates
   * @param xScale {Number} the scale to map model to view in the x-dimension
   * @param yScale {Number} the scale to map model to view in the y-dimension
   * @return the resultant ModelViewTransform2
   */
  ModelViewTransform2.createOffsetXYScaleMapping = function( offset, xScale, yScale ) {
    return new ModelViewTransform2( Matrix3.affine( xScale, 0, 0, yScale, offset.x, offset.y ) );
  };

  /**
   * Creates a shearless ModelViewTransform that maps the specified model point to the specified view point, with the given x and y scales.
   *
   * @param modelPoint {Vector2} the reference point in the model which maps to the specified view point
   * @param viewPoint  {Vector2} the reference point in the view
   * @param xScale     {Number} the amount to scale in the x direction
   * @param yScale     {Number} the amount to scale in the y direction
   * @return the resultant ModelViewTransform2
   */
  ModelViewTransform2.createSinglePointXYScaleMapping = function( modelPoint, viewPoint, xScale, yScale ) {
    // mx * scale + ox = vx
    // my * scale + oy = vy
    var offsetX = viewPoint.x - modelPoint.x * xScale;
    var offsetY = viewPoint.y - modelPoint.y * yScale;
    return this.createOffsetXYScaleMapping( new Vector2( offsetX, offsetY ), xScale, yScale );
  };

  /**
   * Creates a shearless ModelViewTransform that maps the specified model point to the specified view point, with the given scale factor for both x and y dimensions.
   *
   * @param modelPoint {Vector2} the reference point in the model which maps to the specified view point
   * @param viewPoint  {Vector2} the reference point in the view
   * @param scale      {Number} the amount to scale in the x and y directions
   * @return the resultant ModelViewTransform2
   */
  ModelViewTransform2.createSinglePointScaleMapping = function( modelPoint, viewPoint, scale ) {
    return this.createSinglePointXYScaleMapping( modelPoint, viewPoint, scale, scale );
  };

  /**
   * Creates a shearless ModelViewTransform that maps the specified model point to the specified view point, with the given scale factor for both x and y dimensions,
   * but inverting the y axis so that +y in the model corresponds to -y in the view.
   * Inverting the y axis is commonly necessary since +y is usually up in textbooks and -y is down in pixel coordinates.
   *
   * @param modelPoint {Vector2} the reference point in the model which maps to the specified view point
   * @param viewPoint  {Vector2} the reference point in the view
   * @param scale      {Number} the amount to scale in the x and y directions
   * @return the resultant ModelViewTransform2
   */
  ModelViewTransform2.createSinglePointScaleInvertedYMapping = function( modelPoint, viewPoint, scale ) {
    return this.createSinglePointXYScaleMapping( modelPoint, viewPoint, scale, -scale );
  };

  /**
   * Creates a shearless ModelViewTransform that maps the specified rectangle in the model to the specified rectangle in the view,
   * so that any point x% of the way across and y% down in the model rectangle will be mapped to the corresponding point x% across and y% down in the view rectangle.
   * Linear extrapolation is performed outside of the rectangle bounds.
   *
   * @param modelBounds {Bounds2} the reference rectangle in the model, must have area > 0
   * @param viewBounds  {Bounds2} the reference rectangle in the view, must have area > 0
   * @return the resultant ModelViewTransform2
   */
  ModelViewTransform2.createRectangleMapping = function( modelBounds, viewBounds ) {
    var m00 = viewBounds.width / modelBounds.width;
    var m02 = viewBounds.x - m00 * modelBounds.x;
    var m11 = viewBounds.height / modelBounds.height;
    var m12 = viewBounds.y - m11 * modelBounds.y;
    return new ModelViewTransform2( Matrix3.affine( m00, 0, 0, m11, m02, m12 ) );
  };

  /**
   * Creates a shearless ModelViewTransform that maps the specified rectangle in the model to the specified rectangle in the view,
   * so that any point x% of the way across and y% down in the model rectangle will be mapped to the corresponding point x% across and (100-y)% down in the view rectangle.
   * Linear extrapolation is performed outside of the rectangle bounds.
   * Inverting the y axis is commonly necessary since +y is usually up in textbooks and -y is down in pixel coordinates.
   *
   * @param modelBounds {Bounds2} the reference rectangle in the model, must have area > 0
   * @param viewBounds  {Bounds2} the reference rectangle in the view, must have area > 0
   * @return the resultant ModelViewTransform2
   */
  ModelViewTransform2.createRectangleInvertedYMapping = function( modelBounds, viewBounds ) {
    var m00 = viewBounds.width / modelBounds.width;
    var m02 = viewBounds.x - m00 * modelBounds.x;
    var m11 = -viewBounds.height / modelBounds.height;
    // vY == (mY + mHeight) * m11 + m12
    var m12 = viewBounds.y - m11 * modelBounds.getMaxY();
    return new ModelViewTransform2( Matrix3.affine( m00, 0, 0, m11, m02, m12 ) );
  };

  return ModelViewTransform2;
} );

define("image!BEERS_LAW_LAB/../images/Concentration-icon.jpg", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/jpeg;base64,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lfAea8mmPgtYvMctsjvJVVcnOAN3AGeBTf8AhkX4D/8AQnt/4HS//FV6x+fWR+Rf/CWt/wA9jR/wlrf89jX66f8ADIvwH/6E9v8AwOl/+Kqvc/sk/AiPTbiRfB7BkiYg/bpeoB/2qAsj8mdP8UTS34Acnn1r6M8A69Ol/bushByCK+Q9DRTqn0avp/wOg+0wY7kU1uS9z9IvCmqG98MwStyxQZ571zHjsIdMc9Tn+lTeBBjwhb4z0rM8ds39ngZOM01oxHK/ATUjaftL67pIJEV/o7SkerRSJtP5SP8AnX2jXwb8Fif+GzLPk86Xcg+/C/4V95UnuWtgooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACqt9/yB7v8A64v/AOgmrVcL8RPHHhv4e/C6/wDEPinUFsNPUeSh27mkkcEKir/ExwTj0BoA/AnQv+Qk3+9X074I/wCPq3+or5w0LTLtr3eke8E5GDX0b4QW+t54SdNvJQMfcXOaa3Jep+gXgb/kULf6Vk+O/wDjyH1rnPCXjb7F4eht5PDfiNnA6pa5H86h8U+JZ9Ts8W/hvxApH9+3xT6knO/Bb/k8yx/7Blz/ACWvvSvzw+GOtw+G/wBqHT9b8QWt5pOlm0mt2uJo/lRnAC7sdBx1r9C0dZI1dGDIwBVgcgg9xSe5a2JKKKKQwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAr8/P2/71Y/hJ8PtOEpWSfWJphHnhgkQUnHt5g/Ov0Dr8k/24vFia9+1NovhS3lEkHh/TF85Q2cTzkSOP++BDQB8x+GRcecm1mr6u+Hkd+9/AI5CDx1Ga+cfC9p88eR6V9ZfDuIRahASPSmtzM+ufDsF8mjxCaVXbaOdoq7qsNybBgrruI4OKl0eYfYk5BBWrd8we1b6UgPkH4hRXoFyHYkjOcCvu34fXS3vwJ8GXSuZBJolqSxPJPkoDn3zmvkfx9YiaSfC/eFe7fs+62NT/Z+t9NkYG60e6ktHHfYT5iH6bXC/8BNN7lRPc6KKKRQUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAV+CfxTTXG/aX8bXfiaGeHXJdYne4SYEYy52hc/wAO3AXHG3GK/eyuS1vwJ4I8T6jHeeJfB3hXxDdomxJ9T0mC5dV67QzqSB7UCex+GmheJIdOKi6hZgD95PT6V7x4V+LfhfTZo2uvPCjGcDBr9Ov+FQfCX/olvw6/8Ju1/wDjdH/CoPhL/wBEt+HX/hN2v/xundi5T5M0j9pD4cR2yJLcXcZAx1FbUv7RfwzkgIXUJskd8V9M/wDCoPhL/wBEt+HX/hN2v/xuj/hUHwl/6Jb8Ov8Awm7X/wCN0XY7I+G/E/xt8CXyt9nuyTjuc5r0T9lXxNNr/wAWPF50uC7Ph3+zkaecxkReesgEahj/ABbWkOB2HPavqD/hUHwl/wCiW/Dr/wAJu1/+N12Gk6Jo3h/RY9N0HSdN0TTkJMdrYWqQRKScnCqABk+1ILI1qKKKBhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAH//2Q==';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * The 'Concentration' screen. Conforms to the contract specified in joist/Screen.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/concentration/ConcentrationScreen',['require','BEERS_LAW_LAB/concentration/model/ConcentrationModel','BEERS_LAW_LAB/concentration/view/ConcentrationView','SCENERY/nodes/Image','PHETCOMMON/view/ModelViewTransform2','BEERS_LAW_LAB/beers-law-lab-strings','DOT/Vector2','image!BEERS_LAW_LAB/../images/Concentration-icon.jpg'], function( require ) {
  

  // imports
  var ConcentrationModel = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationModel' );
  var ConcentrationView = require( 'BEERS_LAW_LAB/concentration/view/ConcentrationView' );
  var Image = require( 'SCENERY/nodes/Image' );
  var ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var Vector2 = require( 'DOT/Vector2' );

  // images
  var concentrationImage = require( 'image!BEERS_LAW_LAB/../images/Concentration-icon.jpg' );

  function ConcentrationScreen() {

    this.name = strings.concentration;
    this.icon = new Image( concentrationImage );
    this.backgroundColor = 'white';

    var mvt = ModelViewTransform2.createIdentity();

    this.createModel = function() {
      return new ConcentrationModel();
    };

    this.createView = function( model ) {
      return new ConcentrationView( model, mvt );
    };
  }

  return ConcentrationScreen;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model for computing the absorbance (and transmittance) of light passing through a solution in a cuvette.
 * <p>
 * Absorbance model: A = abC
 * <p>
 * Transmittance model: T = 10^A
 * <p>
 * where:
 * <ul>
 * <li>A is absorbance
 * <li>T is transmittance (1=fully transmitted, 0=fully absorbed)
 * <li>a is molar absorptivity (1/(cm*M))
 * <li>b is path length (cm)
 * <li>C is concentration (M)
 * </ul>
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/Absorbance',['require','AXON/Property'], function( require ) {
  

  // imports
  var Property = require( 'AXON/Property' );

  /**
   * @param {Light} light
   * @param {Property<BeersLawSolution>} solution
   * @param {Cuvette} cuvette
   * @constructor
   */
  function Absorbance( light, solution, cuvette ) {

    var thisAbsorbance = this;

    // a: molar absorptivity, units=1/(cm*M)
    {
      var computeMolarAbsorptivity = function() {
        return solution.get().molarAbsorptivityData.wavelengthToMolarAbsorptivity( light.wavelength.get() );
      };

      thisAbsorbance.molarAbsorptivity = new Property( computeMolarAbsorptivity() );

      // dependencies from which this property is derived:
      var updateMolarAbsorptivity = function() {
        thisAbsorbance.molarAbsorptivity.set( computeMolarAbsorptivity() );
      };
      solution.link( updateMolarAbsorptivity );
      light.wavelength.link( updateMolarAbsorptivity );
    }

    // b: path length, synonymous with cuvette width, units=cm
    {
      var computePathLength = function() {
        return cuvette.width.get();
      };

      thisAbsorbance.pathLength = new Property( computePathLength() );

      // dependencies from which this property is derived:
      var updatePathLength = function() {
        thisAbsorbance.pathLength.set( computePathLength() );
      };
      cuvette.width.link( updatePathLength );
    }

    // C: concentration, units=M
    {
      thisAbsorbance.concentration = new Property( solution.get().concentration.get() );

      // Observe the concentration property of the current solution.
      var updateConcentration = function( concentration ) {
        thisAbsorbance.concentration.set( concentration );
      };
      solution.get().concentration.link( updateConcentration );

      // Rewire the concentration observer when the solution changes.
      solution.link( function( newSolution, oldSolution ) {
        if ( oldSolution !== null ) {
          oldSolution.concentration.unlink( updateConcentration );
        }
        newSolution.concentration.link( updateConcentration );
      } );
    }

    // compute absorbance: A = abC
    {
      var computeAbsorbance = function() {
        return getAbsorbance( thisAbsorbance.molarAbsorptivity.get(), thisAbsorbance.pathLength.get(), thisAbsorbance.concentration.get() );
      };

      thisAbsorbance.value = new Property( computeAbsorbance() );

      // dependencies from which this property is derived:
      var updateAbsorbance = function() {
        thisAbsorbance.value.set( computeAbsorbance() );
      };
      thisAbsorbance.molarAbsorptivity.link( updateAbsorbance );
      thisAbsorbance.pathLength.link( updateAbsorbance );
      thisAbsorbance.concentration.link( updateAbsorbance );
    }
  }

  Absorbance.prototype = {

    // Gets absorbance for a specified path length.
    getAbsorbanceAt: function( pathLength ) {
      return getAbsorbance( this.molarAbsorptivity.get(), pathLength, this.concentration.get() );
    },

    // Gets transmittance for a specified path length.
    getTransmittanceAt: function( pathLength ) {
      return getTransmittance( this.getAbsorbanceAt( pathLength ) );
    },

    // Converts absorbance to transmittance.
    getTransmittance: function() {
      return getTransmittance( this.value.get() );
    }
  };

  /*
   * General model of absorbance: A = abC
   * @param {Number} molarAbsorptivity
   * @param {Number} pathLength
   * @param {Number} concentration
   * @return {Number}
   */
  var getAbsorbance = function( molarAbsorptivity, pathLength, concentration ) {
    return molarAbsorptivity * pathLength * concentration;
  };

  /*
   * General model of transmittance: T = 10^A
   * @param {Number} absorbance
   * @return {Number}
   */
  var getTransmittance = function( absorbance ) {
    return Math.pow( 10, -absorbance );
  };

  return Absorbance;
} );






// Copyright 2002-2013, University of Colorado Boulder

/**
 * Detector for absorbance (A) and percent transmittance (%T)
 * of light passing through a solution in a cuvette.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/ATDetector',['require','PHET_CORE/inherit','BEERS_LAW_LAB/common/model/Movable','AXON/Property'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var Movable = require( 'BEERS_LAW_LAB/common/model/Movable' );
  var Property = require( 'AXON/Property' );

  /**
   * The probe, whose position indicates where the measurement is being made.
   * @param {Vector2} location
   * @param {Bounds2} dragBounds
   * @param {Number} sensorDiameter cm
   * @constructor
   */
  function Probe( location, dragBounds, sensorDiameter ) {

    var thisProbe = this;
    Movable.call( thisProbe, location, dragBounds );

    thisProbe.sensorDiameter = sensorDiameter;
  }

  inherit( Movable, Probe, {

    getMinY: function() {
      return this.location.get().y - ( this.sensorDiameter / 2 );
    },

    getMaxY: function() {
      return this.location.get().y + ( this.sensorDiameter / 2 );
    }
  } );

  /**
   * @param {Vector2} bodyLocation
   * @param {Bounds2} bodyDragBounds
   * @param {Vector2} probeLocation
   * @param {Bounds2} probeDragBounds
   * @param {Light} light
   * @param {Cuvette} cuvette
   * @param {Absorbance} absorbance
   * @constructor
   */
  function ATDetector( bodyLocation, bodyDragBounds, probeLocation, probeDragBounds, light, cuvette, absorbance ) {

    var thisDetector = this;

    thisDetector.light = light;
    thisDetector.mode = new Property( ATDetector.Mode.TRANSMITTANCE );
    thisDetector.body = new Movable( bodyLocation, bodyDragBounds );
    thisDetector.probe = new Probe( probeLocation, probeDragBounds, 0.57 );

    // Computes the displayed value, NaN if the light is off or the probe is outside the beam.
    var computeValue = function() {
      var value = NaN;
      if ( thisDetector.probeInBeam() ) {
        // path length is between 0 and cuvette width
        var pathLength = Math.min( Math.max( 0, thisDetector.probe.location.get().x - cuvette.location.x ), cuvette.width.get() );
        if ( thisDetector.mode.get() === ATDetector.Mode.ABSORBANCE ) {
          value = absorbance.getAbsorbanceAt( pathLength );
        }
        else {
          value = 100 * absorbance.getTransmittanceAt( pathLength );
        }
      }
      return value;
    };

    thisDetector.value = new Property( computeValue() );

    // observer dependencies for the value
    var updateValue = function() {
      thisDetector.value.set( computeValue() );
    };
    thisDetector.probe.location.link( updateValue );
    thisDetector.light.on.link( updateValue );
    thisDetector.probe.location.link( updateValue );
    thisDetector.mode.link( updateValue );
    absorbance.value.link( updateValue );
  }

  ATDetector.prototype = {

    reset: function() {
      this.body.reset();
      this.probe.reset();
      this.mode.reset();
    },

    // Is the probe in some segment of the beam?
    probeInBeam: function() {
      return this.light.on.get() &&
             ( this.probe.getMinY() < this.light.getMinY() ) &&
             ( this.probe.getMaxY() > this.light.getMaxY() ) &&
             ( this.probe.location.get().x > this.light.location.x );
    }
  };

  // Modes for the detector
  ATDetector.Mode = {
    'TRANSMITTANCE': 0,
    'ABSORBANCE': 1
  };

  return ATDetector;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Converts a wavelength to a visible color.
 * If the wavelength is not in the visible spectrum, null is returned.
 */
define('SCENERY_PHET/VisibleColor',['require','ASSERT/assert','SCENERY/util/Color'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var Color = require( 'SCENERY/util/Color' );

  function VisibleColor() {}

  // public constants
  VisibleColor.MIN_WAVELENGTH = 380;
  VisibleColor.MAX_WAVELENGTH = 780;

  /**
   * Creates a table that is used to map wavelength to Color.
   * @return {Array<Color>}
   */
  var createColorTable = function() {

    var colorTable = [];

    // Allocate the color-lookup array.
    var numWavelengths = Math.floor( VisibleColor.MAX_WAVELENGTH - VisibleColor.MIN_WAVELENGTH + 1 );

    // Populate the color array.
    var wl;
    var r, g, b;
    for ( var i = 0; i < numWavelengths; i++ ) {
      // Create the RGB component values.
      wl = VisibleColor.MIN_WAVELENGTH + i;
      r = g = b = 0;

      // Determine the RGB component values.
      if ( wl >= 380 && wl <= 440 ) {
        r = -1 * ( wl - 440 ) / ( 440 - 380 );
        g = 0;
        b = 1;
      }
      else if ( wl > 440 && wl <= 490 ) {
        r = 0;
        g = ( wl - 440 ) / ( 490 - 440 );
        b = 1;
      }
      else if ( wl > 490 && wl <= 510 ) {
        r = 0;
        g = 1;
        b = -1 * ( wl - 510 ) / ( 510 - 490 );
      }
      else if ( wl > 510 && wl <= 580 ) {
        r = ( wl - 510 ) / ( 580 - 510 );
        g = 1;
        b = 0;
      }
      else if ( wl > 580 && wl <= 645 ) {
        r = 1;
        g = -1 * ( wl - 645 ) / ( 645 - 580 );
        b = 0;
      }
      else if ( wl > 645 && wl <= 780 ) {
        r = 1;
        g = 0;
        b = 0;
      }

      // Let the intensity fall off near the vision limits.
      var intensity;
      if ( wl > 645 ) {
        intensity = 0.3 + 0.7 * ( 780 - wl ) / ( 780 - 645 );
      }
      else if ( wl < 420 ) {
        intensity = 0.3 + 0.7 * ( wl - 380 ) / ( 420 - 380 );
      }
      else {
        intensity = 1;
      }
      var red = Math.round( 255 * ( intensity * r ) );
      var green = Math.round( 255 * ( intensity * g ) );
      var blue = Math.round( 255 * ( intensity * b ) );
      var alpha = 1;

      // Add the color to the lookup array.
      colorTable[i] = new Color( red, green, blue, alpha );
    }

    return colorTable;
  };

  // Eagerly create the color table.
  var COLOR_TABLE = createColorTable();

  /**
   * Converts a wavelength to a visible color.
   * @param {Number} wavelength will be rounded to the closest integer value
   * @return {Number|null} null if wavelength is not in the visible spectrum
   */
  VisibleColor.wavelengthToColor = function( wavelength ) {
    if ( wavelength < VisibleColor.MIN_WAVELENGTH || wavelength > VisibleColor.MAX_WAVELENGTH ) {
      return null; // Wavelength is not visible.
    }
    else {
      return COLOR_TABLE[ Math.round( wavelength ) - VisibleColor.MIN_WAVELENGTH ];
    }
  };

  return VisibleColor;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model of the light as a solid beam.
 * Changes in wavelength affect the entire beam instantaneously.
 * Consists of 3 segments: left (between light and cuvette), center (inside cuvette), and right (to right of cuvette).
 * Beam may be intercepted at any point by the Absorbance-Transmittance detector.
 * The beam is in the probe if the entire beam is in contact with the probe lens.
 * <p>
 * Unlike most model types, properties are in view coordinates.
 * This makes it convenient to describe the beam's shape and gradient fill.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/Beam',['require','SCENERY/util/Color','SCENERY/util/LinearGradient','AXON/Property','KITE/Shape','DOT/Util','SCENERY_PHET/VisibleColor'], function( require ) {
  

  // imports
  var Color = require( 'SCENERY/util/Color' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var Util = require( 'DOT/Util' );
  var VisibleColor = require( 'SCENERY_PHET/VisibleColor' );

  // constants
  var MAX_LIGHT_WIDTH = 50; // cm, wide enough to be way off the right edge of the play area
  var MAX_LIGHT_ALPHA = 0.78; // transparency of light when transmittance is 1.0 (tuned by eye)
  var MIN_LIGHT_ALPHA = 0.078; // min transparency of light when transmittance is non-zero (tuned by eye)

  /**
   * @param {Light} light
   * @param {Cuvette} cuvette
   * @param {ATDetector} detector
   * @param {Absorbance} absorbance
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function Beam( light, cuvette, detector, absorbance, mvt ) {

    var thisBeam = this;

    // Proper values will be set when observers are registered
    thisBeam.shape = new Property( Shape.rect( 0, 0, 0, 0 ) ); // in view coordinates
    thisBeam.fill = new Property( Color.WHITE ); // gradient, prepared for shape
    thisBeam.visible = new Property( false );

    // Make the beam visible when the light is on.
    light.on.link( function( on ) {
      thisBeam.visible.set( on );
    } );

    // update shape of the beam
    var updateShape = function() {
      if ( thisBeam.visible.get() ) {
        var x = mvt.modelToViewPosition( light.location ).x;
        var y = mvt.modelToViewPosition( light.location ).y - mvt.modelToViewDeltaY( light.lensDiameter / 2 );
        var w = mvt.modelToViewDeltaX( detector.probeInBeam() ? detector.probe.location.get().x - light.location.x : MAX_LIGHT_WIDTH );
        var h = mvt.modelToViewDeltaY( light.lensDiameter );
        thisBeam.shape.set( Shape.rect( x, y, w, h ) );
      }
    };
    cuvette.width.link( updateShape );
    detector.probe.location.link( updateShape );
    thisBeam.visible.link( updateShape );

    // update color of beam, a left-to-right linear gradient that transitions inside the solution
    var updateColor = function() {
      if ( thisBeam.visible.get() ) {
        var baseColor = VisibleColor.wavelengthToColor( light.wavelength.get() );
        var leftColor = baseColor.withAlpha( MAX_LIGHT_ALPHA );
        var rightColor = baseColor.withAlpha( Util.linear( 0, 1, MIN_LIGHT_ALPHA, MAX_LIGHT_ALPHA, absorbance.getTransmittance() ) );
        var x = mvt.modelToViewPosition( cuvette.location ).x;
        var w = mvt.modelToViewDeltaX( cuvette.width.get() );
        thisBeam.fill.set( new LinearGradient( x, 0, x + w, 0 ).addColorStop( 0, leftColor ).addColorStop( 1, rightColor ) );
      }
    };
    light.wavelength.link( updateColor );
    cuvette.width.link( updateColor );
    absorbance.value.link( updateColor );
    thisBeam.visible.link( updateColor );
  }

  return Beam;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Range for a color, with interpolation.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/common/model/ColorRange',['require','ASSERT/assert','SCENERY/util/Color'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var Color = require( 'SCENERY/util/Color' );

  /**
   * @param {Color} min
   * @param {Color} max
   * @constructor
   */
  function ColorRange( min, max ) {
    this.min = min;
    this.max = max;
  }

  ColorRange.prototype = {

    /**
     * Performs a linear interpolation between min and max colors in RGBA colorspace.
     *
     * @param {Number} distance 0-1 (0=min, 1=max)
     * @return {Color}
     */
    interpolateLinear: function( distance ) {
      assert && assert( distance >= 0 && distance <= 1 );
      return Color.interpolateRBGA( this.min, this.max, distance );
    }
  };

  return ColorRange;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Manages the transformation between how concentration values are stored in
 * the model, and how they are presented in the view.
 * <p>
 * In the model, concentration is stored in moles per liter (M) for all solutions.
 * In the view, the concentration units will vary by solution, such that the
 * concentration can be displayed as integral values (no decimals, no scientific notation.)
 * This class specifies the units to be used for displaying the concentration in
 * the view (eg, M, mM, uM) and handles the conversion between model and view units.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/ConcentrationTransform',['require','BEERS_LAW_LAB/beers-law-lab-strings'], function( require ) {
  

  // imports
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );

  /**
   * @param {Number} scale scale factor used to convert model units (Moles, M) to view units
   * @param {String} units
   * @constructor
   */
  function ConcentrationTransform( scale, units ) {
    this.scale = scale;
    this.units = units;
  }

  ConcentrationTransform.prototype = {

    /**
     * Converts from model (M) to view (solution specific).
     * @param {Number} modelConcentration
     * @returns {number}
     */
    modelToView: function( modelConcentration ) {
      return modelConcentration * this.scale;
    },

    /**
     * Converts from view (solution specific) to model (M).
     * @param {Number} viewConcentration
     * @returns {number}
     */
    viewToModel: function( viewConcentration ) {
      return viewConcentration / this.scale;
    }
  };

  ConcentrationTransform.mM = new ConcentrationTransform( 1000, strings.units_mM );
  ConcentrationTransform.uM = new ConcentrationTransform( 1000000, strings.units_uM );

  return ConcentrationTransform;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Container for molar absorptivity data for the visible spectrum of light.
 * Integer precision is sufficient for wavelength, and simplifies lookup.
 * Values were measured experimentally by Julia Chamberlain,
 * and were copy-&-pasted from Unfuddle SVN, beers-law-lab/doc/wavelength-to-molarAbsorptivity.csv.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/MolarAbsorptivityData',['require','ASSERT/assert','SCENERY_PHET/VisibleColor'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var VisibleColor = require( 'SCENERY_PHET/VisibleColor' );

  /**
   * @param {Array<Number>} molarAbsorptivity
   * @constructor
   */
  function MolarAbsorptivityData( molarAbsorptivity ) {
    assert && assert( molarAbsorptivity.length === VisibleColor.MAX_WAVELENGTH - VisibleColor.MIN_WAVELENGTH + 1 );
    this.molarAbsorptivity = molarAbsorptivity;
    this.lambdaMax = MolarAbsorptivityData._getLambdaMax( molarAbsorptivity );
  }

  /*
   * Maps a visible wavelength to its corresponding molar absorptivity.
   * @param {Number} wavelength
   * @return {Number}
   */
  MolarAbsorptivityData.prototype.wavelengthToMolarAbsorptivity = function( wavelength ) {
    assert && assert( wavelength >= VisibleColor.MIN_WAVELENGTH && wavelength <= VisibleColor.MAX_WAVELENGTH );
    var index = Math.floor( wavelength - VisibleColor.MIN_WAVELENGTH );
    return this.molarAbsorptivity[index];
  };

  /*
   * Finds the wavelength with the maximum molar absorptivity.
   * If there are identical maximums, the lower value is returned.
   * @param {Array<Number>} molarAbsorptivity
   * @return {Number}
   */
  MolarAbsorptivityData._getLambdaMax = function( molarAbsorptivity ) {
    var indexMax = 0;
    var molarAbsorptivityMax = molarAbsorptivity[indexMax];
    for ( var i = 0; i < molarAbsorptivity.length; i++ ) {
      if ( molarAbsorptivity[i] > molarAbsorptivityMax ) {
        molarAbsorptivityMax = molarAbsorptivity[i];
        indexMax = i;
      }
    }
    return indexMax + VisibleColor.MIN_WAVELENGTH;
  };

  MolarAbsorptivityData.DRINK_MIX = new MolarAbsorptivityData( new Array(
    1.09, 1.11, 1.11, 1.14, 1.16, 1.17, 1.18, 1.20, 1.21, 1.23, 1.24, 1.25, 1.24, 1.25, 1.28, 1.30,
    1.31, 1.33, 1.34, 1.33, 1.34, 1.36, 1.39, 1.40, 1.39, 1.40, 1.41, 1.42, 1.42, 1.42, 1.41, 1.43,
    1.44, 1.44, 1.46, 1.47, 1.47, 1.49, 1.51, 1.51, 1.53, 1.53, 1.49, 1.51, 1.52, 1.54, 1.56, 1.56,
    1.54, 1.54, 1.56, 1.57, 1.57, 1.60, 1.59, 1.59, 1.61, 1.62, 1.63, 1.64, 1.66, 1.69, 1.72, 1.75,
    1.78, 1.80, 1.85, 1.90, 1.93, 1.97, 2.02, 2.06, 2.10, 2.16, 2.22, 2.28, 2.33, 2.40, 2.46, 2.52,
    2.58, 2.65, 2.72, 2.80, 2.85, 2.89, 2.96, 3.04, 3.13, 3.19, 3.27, 3.35, 3.40, 3.46, 3.53, 3.59,
    3.67, 3.72, 3.78, 3.87, 3.94, 3.99, 4.05, 4.11, 4.17, 4.26, 4.31, 4.34, 4.38, 4.50, 4.58, 4.62,
    4.69, 4.73, 4.77, 4.81, 4.84, 4.86, 4.88, 4.93, 4.97, 4.99, 5.02, 5.03, 5.03, 5.03, 5.04, 5.04,
    5.06, 5.06, 5.06, 5.05, 5.05, 5.05, 5.06, 5.01, 4.98, 4.96, 4.95, 4.95, 4.93, 4.93, 4.92, 4.87,
    4.84, 4.82, 4.78, 4.76, 4.72, 4.65, 4.60, 4.54, 4.49, 4.44, 4.38, 4.32, 4.25, 4.19, 4.11, 4.01,
    3.92, 3.82, 3.71, 3.63, 3.53, 3.39, 3.27, 3.15, 3.02, 2.93, 2.82, 2.70, 2.55, 2.42, 2.30, 2.17,
    2.07, 1.96, 1.84, 1.72, 1.61, 1.51, 1.43, 1.34, 1.24, 1.16, 1.10, 1.05, 0.96, 0.87, 0.80, 0.76,
    0.70, 0.65, 0.62, 0.58, 0.55, 0.52, 0.49, 0.46, 0.44, 0.40, 0.38, 0.37, 0.36, 0.35, 0.33, 0.32,
    0.32, 0.31, 0.32, 0.30, 0.29, 0.29, 0.29, 0.29, 0.28, 0.26, 0.26, 0.25, 0.25, 0.25, 0.23, 0.26,
    0.28, 0.27, 0.25, 0.25, 0.26, 0.26, 0.24, 0.24, 0.24, 0.25, 0.25, 0.26, 0.25, 0.25, 0.24, 0.24,
    0.24, 0.23, 0.24, 0.26, 0.27, 0.26, 0.23, 0.24, 0.24, 0.24, 0.25, 0.24, 0.24, 0.24, 0.24, 0.26,
    0.26, 0.25, 0.24, 0.24, 0.25, 0.26, 0.25, 0.25, 0.25, 0.22, 0.23, 0.26, 0.26, 0.23, 0.23, 0.24,
    0.23, 0.21, 0.23, 0.25, 0.24, 0.24, 0.24, 0.26, 0.25, 0.22, 0.21, 0.23, 0.25, 0.27, 0.26, 0.23,
    0.23, 0.24, 0.22, 0.22, 0.22, 0.21, 0.24, 0.25, 0.24, 0.26, 0.25, 0.23, 0.24, 0.24, 0.24, 0.24,
    0.23, 0.24, 0.26, 0.24, 0.22, 0.23, 0.21, 0.23, 0.26, 0.24, 0.24, 0.24, 0.23, 0.24, 0.24, 0.26,
    0.27, 0.23, 0.24, 0.24, 0.22, 0.21, 0.22, 0.25, 0.24, 0.23, 0.23, 0.23, 0.22, 0.20, 0.20, 0.22,
    0.22, 0.21, 0.21, 0.21, 0.22, 0.22, 0.23, 0.23, 0.21, 0.21, 0.23, 0.24, 0.25, 0.24, 0.25, 0.21,
    0.20, 0.20, 0.19, 0.20, 0.22, 0.22, 0.24, 0.24, 0.25, 0.24, 0.25, 0.25, 0.25, 0.26, 0.26, 0.23,
    0.22, 0.24, 0.24, 0.21, 0.22, 0.23, 0.25, 0.25, 0.20, 0.18, 0.21, 0.23, 0.21, 0.22, 0.21, 0.21,
    0.20, 0.20, 0.24, 0.22, 0.22, 0.22, 0.24, 0.22, 0.21, 0.22, 0.20, 0.20, 0.19, 0.19, 0.18, 0.22,
    0.22
  ) );

  MolarAbsorptivityData.COBALT_II_NITRATE = new MolarAbsorptivityData( new Array(
    0.32, 0.27, 0.23, 0.18, 0.15, 0.13, 0.12, 0.1, 0.07, 0.05, 0.05, 0.03, 0.03, 0.03, 0.03, 0.02, 0.02,
    0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.03, 0.03, 0.03, 0.03, 0.05, 0.05,
    0.05, 0.05, 0.07, 0.07, 0.07, 0.08, 0.08, 0.1, 0.1, 0.1, 0.12, 0.12, 0.13, 0.13, 0.15, 0.15, 0.17, 0.18,
    0.2, 0.2, 0.22, 0.22, 0.23, 0.23, 0.25, 0.27, 0.27, 0.28, 0.28, 0.3, 0.3, 0.32, 0.32, 0.33, 0.35, 0.35,
    0.37, 0.38, 0.4, 0.42, 0.43, 0.45, 0.47, 0.48, 0.5, 0.53, 0.55, 0.58, 0.6, 0.63, 0.67, 0.7, 0.73, 0.77,
    0.8, 0.85, 0.88, 0.93, 0.97, 1, 1.05, 1.1, 1.15, 1.2, 1.27, 1.32, 1.38, 1.43, 1.5, 1.57, 1.63, 1.7, 1.77,
    1.83, 1.92, 1.98, 2.07, 2.13, 2.22, 2.28, 2.35, 2.42, 2.5, 2.57, 2.64, 2.69, 2.65, 2.67, 2.84, 2.92,
    2.97, 3.02, 3.07, 3.1, 3.14, 3.19, 3.24, 3.29, 3.32, 3.37, 3.4, 3.45, 3.49, 3.54, 3.57, 3.59, 3.62,
    3.65, 3.67, 3.7, 3.74, 3.75, 3.8, 3.84, 3.87, 3.92, 3.97, 4.02, 4.07, 4.1, 4.15, 4.2, 4.25, 4.3, 4.35,
    4.4, 4.44, 4.49, 4.54, 4.57, 4.6, 4.64, 4.65, 4.69, 4.7, 4.72, 4.72, 4.72, 4.72, 4.72, 4.7, 4.7, 4.67,
    4.65, 4.62, 4.59, 4.55, 4.5, 4.45, 4.4, 4.35, 4.29, 4.22, 4.15, 4.07, 3.87, 3.85, 3.85, 3.77, 3.67,
    3.59, 3.52, 3.4, 3.3, 3.22, 3.12, 3, 2.9, 2.79, 2.72, 2.62, 2.54, 2.43, 2.33, 2.25, 2.17, 2.08,
    2, 1.91, 1.83, 1.74, 1.66, 1.57, 1.49, 1.4, 1.32, 1.24, 1.15, 1.15, 1.13, 1.07, 1.02, 0.97,
    0.93, 0.9, 0.85, 0.8, 0.77, 0.72, 0.7, 0.67, 0.63, 0.62, 0.58, 0.57, 0.53, 0.52, 0.5, 0.48, 0.47,
    0.47, 0.45, 0.43, 0.42, 0.42, 0.4, 0.4, 0.38, 0.38, 0.37, 0.37, 0.37, 0.37, 0.35, 0.35, 0.35, 0.35,
    0.35, 0.33, 0.33, 0.33, 0.33, 0.32, 0.32, 0.32, 0.32, 0.32, 0.32, 0.32, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3,
    0.3, 0.3, 0.3, 0.3, 0.3, 0.28, 0.28, 0.28, 0.3, 0.28, 0.3, 0.3, 0.28, 0.28, 0.28, 0.28, 0.28, 0.28,
    0.28, 0.27, 0.28, 0.27, 0.27, 0.27, 0.27, 0.27, 0.27, 0.27, 0.27, 0.25, 0.25, 0.25, 0.25, 0.25,
    0.25, 0.23, 0.23, 0.23, 0.23, 0.22, 0.22, 0.22, 0.22, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.18, 0.18,
    0.18, 0.17, 0.17, 0.17, 0.17, 0.17, 0.17, 0.17, 0.15, 0.17, 0.15, 0.13, 0.15, 0.13, 0.13, 0.13,
    0.13, 0.15, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.1, 0.12, 0.1, 0.1, 0.1, 0.08, 0.1, 0.08, 0.08,
    0.08, 0.1, 0.08, 0.08, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.05, 0.07, 0.07, 0.05, 0.07,
    0.07, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.03, 0.03, 0.03, 0.03, 0.05,
    0.05, 0.03, 0.03, 0.03, 0.03
  ) );

  MolarAbsorptivityData.COBALT_CHLORIDE = new MolarAbsorptivityData( new Array(
    7.01, 6.97, 6.94, 6.91, 6.87, 6.84, 6.81, 6.77, 6.73, 6.7, 6.66, 6.64, 6.62, 6.59, 6.55, 6.53, 6.5, 6.46, 6.43, 6.4,
    6.38, 6.34, 6.32, 6.29, 6.28, 6.24, 6.22, 6.19, 6.17, 6.14, 6.11, 6.09, 6.07, 6.06, 6.02, 6, 5.98, 5.96, 5.93, 5.91,
    5.89, 5.87, 5.86, 5.83, 5.82, 5.8, 5.78, 5.77, 5.75, 5.74, 5.71, 5.7, 5.69, 5.67, 5.66, 5.65, 5.64, 5.61, 5.6, 5.58,
    5.57, 5.55, 5.54, 5.53, 5.51, 5.49, 5.48, 5.47, 5.45, 5.45, 5.44, 5.43, 5.41, 5.41, 5.4, 5.39, 5.39, 5.38, 5.38, 5.38,
    5.38, 5.37, 5.37, 5.38, 5.38, 5.39, 5.39, 5.4, 5.41, 5.43, 5.44, 5.46, 5.47, 5.48, 5.5, 5.51, 5.54, 5.56, 5.58, 5.61,
    5.64, 5.67, 5.69, 5.72, 5.76, 5.79, 5.83, 5.87, 5.91, 5.94, 5.99, 6.03, 6.08, 6.11, 6.14, 6.19, 6.23, 6.27, 6.3, 6.33,
    6.3, 6.3, 6.4, 6.44, 6.46, 6.49, 6.51, 6.53, 6.54, 6.56, 6.59, 6.61, 6.63, 6.64, 6.66, 6.69, 6.7, 6.72, 6.73, 6.74,
    6.74, 6.75, 6.76, 6.77, 6.78, 6.78, 6.81, 6.83, 6.84, 6.86, 6.88, 6.91, 6.93, 6.95, 6.97, 7.01, 7.03, 7.06, 7.08,
    7.11, 7.13, 7.14, 7.16, 7.18, 7.19, 7.2, 7.22, 7.22, 7.22, 7.23, 7.22, 7.2, 7.19, 7.18, 7.16, 7.15, 7.12, 7.08, 7.05,
    7.02, 6.97, 6.93, 6.88, 6.84, 6.78, 6.73, 6.67, 6.61, 6.54, 6.39, 6.35, 6.34, 6.27, 6.19, 6.11, 6.04, 5.96, 5.87,
    5.79, 5.71, 5.61, 5.53, 5.43, 5.37, 5.29, 5.2, 5.13, 5.04, 4.96, 4.89, 4.82, 4.74, 4.67, 4.59, 4.52, 4.45, 4.37,
    4.3, 4.22, 4.15, 4.07, 4, 4, 3.99, 3.93, 3.87, 3.82, 3.78, 3.75, 3.7, 3.66, 3.61, 3.58, 3.55, 3.51, 3.49, 3.46, 3.43,
    3.4, 3.38, 3.35, 3.33, 3.3, 3.28, 3.27, 3.25, 3.23, 3.22, 3.19, 3.18, 3.16, 3.15, 3.14, 3.13, 3.12, 3.11, 3.09, 3.07,
    3.06, 3.05, 3.04, 3.03, 3.02, 3.02, 2.99, 2.99, 2.97, 2.96, 2.96, 2.95, 2.94, 2.93, 2.92, 2.92, 2.91, 2.9, 2.88, 2.88,
    2.87, 2.86, 2.85, 2.84, 2.84, 2.83, 2.82, 2.82, 2.81, 2.8, 2.8, 2.78, 2.77, 2.76, 2.76, 2.75, 2.74, 2.74, 2.73, 2.72,
    2.71, 2.7, 2.7, 2.67, 2.67, 2.66, 2.66, 2.65, 2.64, 2.64, 2.62, 2.62, 2.61, 2.6, 2.6, 2.59, 2.57, 2.56, 2.55, 2.55,
    2.54, 2.53, 2.52, 2.51, 2.51, 2.5, 2.49, 2.48, 2.48, 2.45, 2.45, 2.44, 2.43, 2.42, 2.42, 2.41, 2.4, 2.39, 2.39, 2.38,
    2.36, 2.35, 2.35, 2.34, 2.32, 2.32, 2.31, 2.3, 2.3, 2.29, 2.29, 2.28, 2.27, 2.25, 2.24, 2.24, 2.23, 2.22, 2.22, 2.21,
    2.21, 2.2, 2.19, 2.18, 2.18, 2.17, 2.17, 2.15, 2.14, 2.13, 2.12, 2.12, 2.12, 2.11, 2.1, 2.1, 2.09, 2.08, 2.08, 2.07,
    2.07, 2.07, 2.04, 2.04, 2.03, 2.02, 2.02, 2.02, 2.01, 2.01, 2, 2, 1.99, 1.98, 1.98, 1.97, 1.97, 1.97, 1.96, 1.94, 1.94,
    1.94, 1.93
  ) );

  MolarAbsorptivityData.POTASSIUM_DICHROMATE = new MolarAbsorptivityData( new Array(
    3245.74, 3305.84, 3365.95, 3396, 3456.11, 3516.21, 3546.27, 3546.27, 3576.32, 3606.37, 3636.43, 3666.48, 3696.53, 3696.53,
    3696.53, 3696.53, 3696.53, 3666.48, 3666.48, 3636.43, 3636.43, 3606.37, 3606.37, 3576.32, 3546.27, 3516.21, 3456.11, 3396,
    3365.95, 3305.84, 3275.79, 3245.74, 3185.63, 3155.58, 3095.47, 3035.36, 2975.26, 2885.1, 2824.99, 2734.83, 2674.73, 2584.57,
    2554.51, 2494.41, 2434.3, 2344.14, 2284.04, 2193.88, 2103.72, 2013.56, 1923.4, 1833.24, 1773.13, 1713.03, 1652.92, 1592.82,
    1532.71, 1442.55, 1382.44, 1322.34, 1262.23, 1202.12, 1142.02, 1111.97, 1051.86, 1021.81, 991.75, 931.65, 901.59, 871.54,
    841.49, 811.43, 811.43, 781.38, 781.38, 751.33, 751.33, 751.33, 721.27, 721.27, 691.22, 691.22, 691.22, 691.22, 691.22,
    691.22, 661.17, 661.17, 661.17, 661.17, 661.17, 661.17, 661.17, 661.17, 631.12, 631.12, 631.12, 631.12, 631.12, 631.12,
    601.06, 601.06, 601.06, 601.06, 601.06, 601.06, 571.01, 571.01, 571.01, 571.01, 571.01, 540.96, 540.96, 540.96, 540.96,
    540.96, 510.9, 510.9, 510.9, 480.85, 450.8, 420.74, 450.8, 480.85, 450.8, 450.8, 450.8, 420.74, 420.74, 420.74, 420.74,
    390.69, 390.69, 390.69, 390.69, 360.64, 360.64, 330.58, 330.58, 330.58, 330.58, 330.58, 330.58, 300.53, 300.53, 300.53,
    300.53, 270.48, 270.48, 270.48, 270.48, 270.48, 270.48, 240.42, 240.42, 240.42, 240.42, 210.37, 210.37, 210.37, 210.37,
    210.37, 210.37, 210.37, 210.37, 180.32, 180.32, 180.32, 180.32, 180.32, 180.32, 180.32, 180.32, 150.27, 150.27, 150.27,
    150.27, 150.27, 150.27, 150.27, 150.27, 150.27, 120.21, 120.21, 150.27, 120.21, 120.21, 120.21, 120.21, 120.21, 120.21,
    120.21, 120.21, 120.21, 120.21, 120.21, 120.21, 120.21, 120.21, 120.21, 120.21, 120.21, 120.21, 120.21, 90.16, 120.21,
    90.16, 90.16, 88.01, 85.87, 83.72, 81.57, 79.43, 77.28, 75.13, 72.99, 70.84, 68.69, 66.55, 64.4, 62.25, 60.11, 60.11,
    180.32, 90.16, 120.21, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16,
    90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 120.21, 90.16, 90.16, 90.16, 90.16,
    90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16,
    90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16,
    90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 90.16, 60.11, 90.16, 60.11, 90.16, 90.16,
    90.16, 60.11, 90.16, 90.16, 90.16, 90.16, 90.16, 60.11, 90.16, 90.16, 60.11, 90.16, 60.11, 90.16, 60.11, 60.11, 90.16,
    60.11, 60.11, 90.16, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 90.16, 60.11, 60.11, 90.16, 90.16, 90.16, 90.16, 60.11,
    60.11, 60.11, 60.11, 60.11, 90.16, 60.11, 60.11, 60.11, 60.11, 90.16, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11,
    60.11, 60.11, 60.11, 90.16, 90.16, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 90.16,
    60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 90.16, 60.11,
    60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11, 60.11
  ) );

  MolarAbsorptivityData.POTASSIUM_CHROMATE = new MolarAbsorptivityData( new Array(
    1737.67, 1805.81, 1908.03, 2010.24, 2146.53, 2248.75, 2350.96, 2453.18, 2589.47, 2725.75, 2862.04, 2964.26, 3100.55,
    3202.76, 3304.98, 3441.26, 3577.55, 3713.84, 3816.06, 3918.27, 4054.56, 4156.78, 4224.92, 4293.06, 4395.28, 4497.49,
    4565.64, 4633.78, 4701.93, 4736, 4736, 4770.07, 4770.07, 4770.07, 4770.07, 4770.07, 4736, 4736, 4667.85, 4633.78,
    4531.57, 4429.35, 4327.14, 4224.92, 4122.7, 4020.49, 3884.2, 3781.98, 3679.77, 3543.48, 3407.19, 3236.83, 3134.62,
    2998.33, 2862.04, 2691.68, 2555.39, 2385.04, 2282.82, 2180.6, 2044.32, 1942.1, 1839.88, 1737.67, 1601.38, 1499.16,
    1431.02, 1328.81, 1226.59, 1158.45, 1090.3, 1022.16, 988.09, 954.01, 919.94, 851.8, 817.73, 783.65, 783.65, 749.58,
    715.51, 681.44, 681.44, 647.37, 647.37, 613.29, 613.29, 579.22, 545.15, 545.15, 545.15, 511.08, 511.08, 477.01, 477.01,
    477.01, 442.94, 442.94, 408.86, 408.86, 408.86, 374.79, 374.79, 340.72, 340.72, 340.72, 306.65, 306.65, 306.65, 306.65,
    272.58, 272.58, 272.58, 272.58, 238.5, 238.5, 238.5, 204.43, 204.43, 204.43, 170.36, 136.29, 170.36, 204.43, 170.36,
    170.36, 170.36, 170.36, 170.36, 170.36, 170.36, 136.29, 136.29, 136.29, 136.29, 136.29, 136.29, 136.29, 136.29, 136.29,
    136.29, 136.29, 136.29, 136.29, 136.29, 136.29, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22,
    102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22,
    102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 68.14, 102.22,
    102.22, 102.22, 102.22, 102.22, 102.22, 68.14, 102.22, 68.14, 102.22, 102.22, 102.22, 102.22, 102.22, 102.22, 68.14,
    68.14, 68.14, 68.14, 68.14, 68.14, 102.22, 68.14, 68.14, 65.71, 63.28, 60.84, 58.41, 55.98, 53.54, 51.11, 48.67, 46.24,
    43.81, 41.37, 38.94, 36.51, 34.07, 34.07, 102.22, 136.29, 136.29, 68.14, 68.14, 68.14, 102.22, 68.14, 68.14, 68.14, 68.14,
    68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 102.22, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14,
    68.14, 102.22, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14,
    68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14,
    68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14,
    68.14, 34.07, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 34.07, 68.14, 68.14, 68.14, 68.14, 34.07, 68.14, 68.14, 68.14,
    68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 34.07, 34.07, 68.14, 34.07, 68.14, 68.14, 34.07,
    68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 68.14, 34.07, 34.07, 34.07, 34.07, 68.14, 34.07, 68.14, 68.14,
    68.14, 68.14, 68.14, 34.07, 68.14, 68.14, 68.14, 34.07, 34.07, 34.07, 68.14, 34.07, 68.14, 68.14, 34.07, 68.14, 68.14,
    34.07, 68.14, 34.07, 34.07, 34.07, 68.14, 68.14, 34.07, 68.14, 34.07, 68.14, 68.14, 34.07, 68.14, 68.14, 34.07, 68.14,
    68.14, 34.07, 34.07, 34.07, 68.14, 68.14, 68.14, 34.07, 68.14, 68.14, 34.07, 68.14, 34.07, 68.14
  ) );

  MolarAbsorptivityData.NICKEL_II_CHLORIDE = new MolarAbsorptivityData( new Array(
    0.31, 0.33, 0.34, 0.36, 0.39, 0.41, 0.43, 0.46, 0.48, 0.51, 0.57, 0.58, 0.63, 0.67, 0.7, 0.75, 0.8, 0.86, 0.92, 0.99,
    1.08, 1.16, 1.25, 1.32, 1.44, 1.56, 1.68, 1.82, 1.95, 2.09, 2.26, 2.41, 2.55, 2.69, 2.86, 3.03, 3.22, 3.41, 3.58,
    3.75, 3.92, 4.09, 4.25, 4.4, 4.56, 4.68, 4.81, 4.93, 5.03, 5.1, 5.19, 5.24, 5.29, 5.31, 5.31, 5.31, 5.31, 5.26,
    5.22, 5.17, 5.1, 5.02, 4.93, 4.85, 4.74, 4.64, 4.52, 4.4, 4.28, 4.16, 4.01, 3.89, 3.77, 3.61, 3.48, 3.34, 3.19,
    3.03, 2.88, 2.72, 2.57, 2.43, 2.29, 2.16, 2.02, 1.9, 1.78, 1.66, 1.54, 1.46, 1.34, 1.25, 1.16, 1.1, 1.03, 0.98,
    0.91, 0.84, 0.79, 0.75, 0.7, 0.67, 0.63, 0.62, 0.6, 0.57, 0.55, 0.53, 0.51, 0.5, 0.48, 0.48, 0.46, 0.45, 0.43,
    0.43, 0.41, 0.41, 0.39, 0.38, 0.33, 0.31, 0.33, 0.33, 0.33, 0.31, 0.29, 0.29, 0.27, 0.26, 0.24, 0.24, 0.22,
    0.21, 0.21, 0.19, 0.17, 0.17, 0.15, 0.14, 0.14, 0.12, 0.12, 0.1, 0.1, 0.09, 0.09, 0.09, 0.07, 0.07, 0.07,
    0.07, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
    0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.07, 0.07, 0.07, 0.07, 0.07, 0.09, 0.07, 0.09, 0.09, 0.09, 0.09, 0.09,
    0.1, 0.1, 0.09, 0.09, 0.1, 0.1, 0.12, 0.12, 0.12, 0.12, 0.12, 0.14, 0.14, 0.14, 0.14, 0.14, 0.14, 0.14,
    0.14, 0.14, 0.14, 0.14, 0.15, 0.16, 0.17, 0.17, 0.18, 0.19, 0.2, 0.2, 0.21, 0.22, 0.23, 0.23, 0.24, 0.24,
    0.27, 0.24, 0.22, 0.22, 0.24, 0.26, 0.26, 0.26, 0.26, 0.27, 0.29, 0.29, 0.31, 0.33, 0.33, 0.33, 0.34, 0.36,
    0.36, 0.38, 0.39, 0.41, 0.43, 0.43, 0.45, 0.46, 0.48, 0.5, 0.5, 0.51, 0.53, 0.55, 0.57, 0.58, 0.6, 0.62,
    0.63, 0.65, 0.67, 0.69, 0.72, 0.74, 0.75, 0.77, 0.79, 0.82, 0.84, 0.87, 0.89, 0.91, 0.94, 0.96, 0.99, 1.01,
    1.04, 1.06, 1.1, 1.11, 1.13, 1.16, 1.2, 1.22, 1.25, 1.27, 1.3, 1.34, 1.35, 1.39, 1.42, 1.44, 1.47, 1.51,
    1.52, 1.56, 1.58, 1.59, 1.63, 1.64, 1.66, 1.7, 1.71, 1.75, 1.76, 1.78, 1.8, 1.83, 1.85, 1.85, 1.88, 1.9,
    1.92, 1.92, 1.94, 1.94, 1.94, 1.94, 1.94, 1.94, 1.94, 1.92, 1.92, 1.9, 1.9, 1.9, 1.9, 1.9, 1.9, 1.88, 1.88,
    1.88, 1.9, 1.88, 1.9, 1.9, 1.9, 1.9, 1.9, 1.92, 1.94, 1.94, 1.94, 1.95, 1.95, 1.97, 1.97, 1.99, 1.99, 2, 2,
    2.02, 2.04, 2.04, 2.06, 2.06, 2.07, 2.09, 2.09, 2.09, 2.12, 2.12, 2.14, 2.14, 2.16, 2.16, 2.17, 2.17, 2.17,
    2.19, 2.19, 2.21, 2.21, 2.23, 2.23, 2.23, 2.23, 2.23, 2.24, 2.24, 2.24, 2.24, 2.24, 2.24, 2.23, 2.23,
    2.23, 2.23, 2.21, 2.21, 2.21, 2.19, 2.19, 2.17, 2.16, 2.14, 2.14, 2.12, 2.11, 2.11
  ) );

  MolarAbsorptivityData.COPPER_SULFATE = new MolarAbsorptivityData( new Array(
    0.23, 0.23, 0.23, 0.22, 0.22, 0.22, 0.22, 0.2, 0.2, 0.18, 0.18, 0.18, 0.18, 0.18, 0.18, 0.18, 0.18,
    0.16, 0.16, 0.16, 0.16, 0.16, 0.16, 0.14, 0.14, 0.14, 0.14, 0.14, 0.14, 0.14, 0.14, 0.14, 0.13,
    0.14, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11,
    0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.11, 0.09, 0.09, 0.09,
    0.11, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.11, 0.09, 0.09, 0.09,
    0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07,
    0.07, 0.07, 0.07, 0.07, 0.05, 0.07, 0.07, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
    0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
    0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
    0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.07, 0.05, 0.07,
    0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.09, 0.09,
    0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.09, 0.11, 0.13, 0.13,
    0.13, 0.13, 0.14, 0.14, 0.14, 0.16, 0.16, 0.16, 0.16, 0.16, 0.18, 0.18, 0.18, 0.2, 0.2, 0.21, 0.22,
    0.23, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.33, 0.34, 0.35, 0.36, 0.36, 0.36, 0.36, 0.34, 0.34,
    0.36, 0.38, 0.38, 0.4, 0.4, 0.4, 0.42, 0.43, 0.45, 0.45, 0.47, 0.47, 0.49, 0.51, 0.52, 0.54, 0.54,
    0.56, 0.58, 0.6, 0.61, 0.63, 0.65, 0.67, 0.69, 0.7, 0.72, 0.74, 0.76, 0.79, 0.79, 0.81, 0.85, 0.87,
    0.89, 0.92, 0.94, 0.96, 0.99, 1.01, 1.03, 1.07, 1.08, 1.12, 1.16, 1.17, 1.21, 1.23, 1.26, 1.3, 1.34,
    1.37, 1.41, 1.43, 1.46, 1.52, 1.55, 1.59, 1.63, 1.66, 1.72, 1.75, 1.79, 1.84, 1.88, 1.92, 1.97, 2.01,
    2.06, 2.1, 2.15, 2.19, 2.24, 2.29, 2.35, 2.38, 2.44, 2.51, 2.55, 2.6, 2.67, 2.73, 2.78, 2.84, 2.89,
    2.96, 3.02, 3.07, 3.13, 3.2, 3.27, 3.32, 3.4, 3.45, 3.5, 3.58, 3.65, 3.72, 3.79, 3.87, 3.94, 4.01,
    4.08, 4.16, 4.23, 4.3, 4.35, 4.44, 4.52, 4.59, 4.66, 4.75, 4.82, 4.91, 4.99, 5.06, 5.15, 5.22, 5.29,
    5.38, 5.46, 5.56, 5.64, 5.71, 5.8, 5.87, 5.96, 6.02, 6.11, 6.2, 6.29, 6.38, 6.45, 6.52, 6.61, 6.68,
    6.77, 6.87, 6.96, 7.01, 7.12, 7.19, 7.26, 7.35, 7.44, 7.52, 7.61, 7.68, 7.75, 7.82, 7.91, 7.99, 8.08,
    8.15, 8.24, 8.31, 8.38, 8.46, 8.55, 8.62, 8.69, 8.76, 8.83, 8.92, 8.98, 9.05, 9.12, 9.2, 9.27, 9.34,
    9.39, 9.47, 9.54, 9.61
  ) );

  MolarAbsorptivityData.POTASSIUM_PERMANGANATE = new MolarAbsorptivityData( new Array(
    1277.2, 1271.32, 1265.43, 1259.55, 1253.66, 1247.78, 1241.89, 1236, 1218.35, 1206.58, 1194.8,
    1183.03, 1171.26, 1159.49, 1141.83, 1130.06, 1112.4, 1088.86, 1065.32, 1047.66, 1024.12,
    1000.58, 977.03, 959.37, 929.95, 906.4, 882.86, 847.55, 824, 794.57, 765.15, 741.6, 712.17,
    694.52, 665.09, 635.66, 612.12, 582.69, 553.26, 529.72, 500.29, 476.74, 453.2, 435.54, 412,
    388.46, 364.92, 347.26, 323.72, 306.06, 282.52, 264.86, 247.2, 235.43, 223.66, 206, 194.23,
    176.57, 170.69, 158.91, 141.26, 135.37, 123.6, 117.71, 111.83, 100.06, 94.17, 88.29, 82.4,
    82.4, 76.51, 70.63, 64.74, 64.74, 64.74, 64.74, 58.86, 58.86, 58.86, 58.86, 58.86, 58.86,
    58.86, 58.86, 58.86, 64.74, 64.74, 64.74, 64.74, 70.63, 70.63, 76.51, 76.51, 82.4, 88.29,
    88.29, 94.17, 100.06, 105.94, 111.83, 117.71, 123.6, 123.6, 129.49, 135.37, 141.26, 147.14,
    158.91, 164.8, 176.57, 188.34, 200.12, 211.89, 223.66, 235.43, 247.2, 258.97, 264.86, 276.63,
    282.52, 276.63, 282.52, 317.83, 347.26, 370.8, 394.34, 423.77, 447.32, 470.86, 494.4, 517.94,
    535.6, 553.26, 570.92, 582.69, 600.35, 618, 641.55, 665.09, 700.4, 735.72, 776.92, 824, 871.09,
    918.17, 953.49, 994.69, 1030, 1059.43, 1077.09, 1094.75, 1106.52, 1118.29, 1130.06, 1147.72,
    1177.15, 1212.46, 1259.55, 1312.52, 1377.26, 1442.01, 1512.63, 1583.26, 1648.01, 1700.98,
    1742.18, 1777.49, 1789.26, 1795.15, 1789.26, 1777.49, 1765.72, 1759.83, 1759.83, 1771.61,
    1795.15, 1836.35, 1895.21, 1959.95, 2030.58, 2107.09, 2183.61, 2248.35, 2307.21, 2354.29,
    2383.72, 2389.61, 2377.84, 2336.64, 2248.35, 2207.15, 2177.72, 2118.86, 2065.89, 2024.69,
    2007.04, 1995.26, 2007.04, 2024.69, 2060.01, 2107.09, 2154.18, 2189.49, 2236.58, 2277.78,
    2301.32, 2307.21, 2295.44, 2222.29, 2149.13, 2075.98, 2002.83, 1929.68, 1856.53, 1783.38,
    1710.23, 1637.08, 1563.92, 1490.77, 1417.62, 1344.47, 1271.32, 1271.32, 1283.09, 1283.09,
    1300.75, 1294.86, 1294.86, 1288.98, 1283.09, 1253.66, 1230.12, 1194.8, 1147.72, 1094.75,
    1030, 959.37, 888.75, 812.23, 741.6, 676.86, 612.12, 559.14, 506.17, 464.97, 429.66, 394.34,
    364.92, 341.37, 323.72, 311.94, 294.29, 288.4, 276.63, 264.86, 258.97, 253.09, 247.2, 241.32,
    241.32, 235.43, 235.43, 229.54, 223.66, 223.66, 217.77, 217.77, 211.89, 211.89, 206, 206,
    200.12, 200.12, 194.23, 194.23, 194.23, 188.34, 188.34, 188.34, 188.34, 182.46, 182.46,
    182.46, 182.46, 176.57, 176.57, 176.57, 176.57, 176.57, 176.57, 170.69, 170.69, 170.69,
    164.8, 170.69, 164.8, 164.8, 158.91, 158.91, 153.03, 158.91, 153.03, 153.03, 147.14,
    153.03, 147.14, 147.14, 141.26, 141.26, 141.26, 135.37, 135.37, 129.49, 135.37, 135.37,
    123.6, 123.6, 123.6, 117.71, 117.71, 117.71, 117.71, 117.71, 111.83, 111.83, 111.83, 105.94,
    105.94, 100.06, 100.06, 100.06, 100.06, 88.29, 94.17, 94.17, 88.29, 88.29, 88.29, 88.29,
    82.4, 82.4, 76.51, 76.51, 76.51, 70.63, 70.63, 70.63, 64.74, 64.74, 64.74, 64.74, 64.74,
    52.97, 58.86, 58.86, 52.97, 52.97, 52.97, 52.97, 47.09, 47.09, 47.09, 47.09, 47.09, 41.2,
    41.2, 41.2, 41.2, 41.2, 41.2, 35.31, 35.31, 35.31, 35.31, 29.43, 35.31, 29.43, 29.43, 29.43,
    29.43, 29.43, 23.54, 23.54, 23.54, 23.54, 23.54, 23.54, 23.54, 23.54, 23.54, 23.54, 23.54,
    23.54, 17.66, 17.66, 17.66, 17.66, 17.66, 17.66, 17.66, 17.66
  ) );

  return MolarAbsorptivityData;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Solution model for the Beer's Law screen.
 * <p/>
 * The numeric values for specific solutions were arrived at by running lab experiments,
 * and are documented in doc/Beers-Law-Lab-design.pdf and doc/BeersLawLabData.xlsx.
 * <p/>
 * Note that this model does not use the Solute model from the Concentration screen, because
 * we have very different needs wrt color scheme, properties, etc.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/BeersLawSolution',['require','BEERS_LAW_LAB/common/BLLSymbols','SCENERY/util/Color','BEERS_LAW_LAB/common/model/ColorRange','BEERS_LAW_LAB/beerslaw/model/ConcentrationTransform','BEERS_LAW_LAB/beerslaw/model/MolarAbsorptivityData','AXON/Property','DOT/Range','BEERS_LAW_LAB/common/model/Solvent','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','DOT/Util'], function( require ) {
  

  // imports
  var BLLSymbols = require( 'BEERS_LAW_LAB/common/BLLSymbols' );
  var Color = require( 'SCENERY/util/Color' );
  var ColorRange = require( 'BEERS_LAW_LAB/common/model/ColorRange' );
  var ConcentrationTransform = require( 'BEERS_LAW_LAB/beerslaw/model/ConcentrationTransform' );
  var MolarAbsorptivityData = require( 'BEERS_LAW_LAB/beerslaw/model/MolarAbsorptivityData' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var Solvent = require( 'BEERS_LAW_LAB/common/model/Solvent' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Util = require( 'DOT/Util' );

  /**
   * @param {String} name
   * @param {String} formula
   * @param {MolarAbsorptivityData} molarAbsorptivityData
   * @param {Range} concentrationRange
   * @param {ConcentrationTransform} concentrationTransform
   * @param {ColorRange} colorRange
   * @param {Color} saturatedColor optional, defaults to colorRange.maxColor
   * @constructor
   */
  function BeersLawSolution( name, formula, molarAbsorptivityData, concentrationRange, concentrationTransform, colorRange, saturatedColor ) {

    var thisSolution = this;

    thisSolution.solvent = Solvent.WATER;
    thisSolution.name = name;
    thisSolution.formula = formula;
    thisSolution.molarAbsorptivityData = molarAbsorptivityData;
    thisSolution.concentration = new Property( concentrationRange.defaultValue );
    thisSolution.concentrationRange = concentrationRange;
    thisSolution.concentrationTransform = concentrationTransform;
    thisSolution.colorRange = colorRange;
    thisSolution.saturatedColor = saturatedColor || colorRange.max;

    // Creates a color that corresponds to a concentration value.
    var createFluidColor = function( concentration ) {
      var color = thisSolution.solvent.color.get();
      if ( concentration > 0 ) {
        var distance = Util.linear( thisSolution.concentrationRange.min, thisSolution.concentrationRange.max, 0, 1, concentration );
        color = thisSolution.colorRange.interpolateLinear( distance );
      }
      return color;
    };
    thisSolution.fluidColor = new Property( createFluidColor( thisSolution.concentration.get() ) );

    // update fluid color when concentration changes
    this.concentration.link( function( concentration ) {
      thisSolution.fluidColor.set( createFluidColor( concentration ) );
    } );
  }

  BeersLawSolution.prototype = {

    reset: function() {
      this.concentration.reset();
    },

    getDisplayName: function() {
      if ( this.formula === this.name ) {
        return this.name;
      }
      return StringUtils.format( strings.pattern_0formula_1name, this.formula, this.name );
    },

    getDisplayConcentration: function( concentration ) {
      return StringUtils.format( strings.pattern_0value_1units,
        this.concentrationTransform.modelToView( concentration ).toFixed( 0 ), this.concentrationTransform.units );
    }
  };

  //-------------------------------------------------------------------------------------------
  // Specific solutions below ...
  //-------------------------------------------------------------------------------------------

  BeersLawSolution.DRINK_MIX = new BeersLawSolution(
    strings.drinkMix,
    BLLSymbols.DRINK_MIX,
    MolarAbsorptivityData.DRINK_MIX,
    new Range( 0, 0.400, 0.100 ),
    ConcentrationTransform.mM,
    new ColorRange( new Color( 255, 225, 225 ), Color.RED )
  );

  BeersLawSolution.COBALT_II_NITRATE = new BeersLawSolution(
    strings.cobaltIINitrate,
    BLLSymbols.COBALT_II_NITRATE,
    MolarAbsorptivityData.COBALT_II_NITRATE,
    new Range( 0, 0.400, 0.100 ),
    ConcentrationTransform.mM,
    new ColorRange( new Color( 255, 225, 225 ), Color.RED ) );

  BeersLawSolution.COBALT_CHLORIDE = new BeersLawSolution(
    strings.cobaltChloride,
    BLLSymbols.COBALT_CHLORIDE,
    MolarAbsorptivityData.COBALT_CHLORIDE,
    new Range( 0, 0.250, 0.100 ),
    ConcentrationTransform.mM,
    new ColorRange( new Color( 255, 242, 242 ), new Color( 255, 106, 106 ) )
  );

  BeersLawSolution.POTASSIUM_DICHROMATE = new BeersLawSolution(
    strings.potassiumDichromate,
    BLLSymbols.POTASSIUM_DICHROMATE,
    MolarAbsorptivityData.POTASSIUM_DICHROMATE,
    new Range( 0, 0.000500, 0.000100 ),
    ConcentrationTransform.uM,
    new ColorRange( new Color( 255, 232, 210 ), new Color( 255, 127, 0 ) )
  );

  BeersLawSolution.POTASSIUM_CHROMATE = new BeersLawSolution(
    strings.potassiumChromate,
    BLLSymbols.POTASSIUM_CHROMATE,
    MolarAbsorptivityData.POTASSIUM_CHROMATE,
    new Range( 0, 0.000400, 0.000100 ),
    ConcentrationTransform.uM,
    new ColorRange( new Color( 255, 255, 199 ), new Color( 255, 255, 0 ) )
  );

  BeersLawSolution.NICKEL_II_CHLORIDE = new BeersLawSolution(
    strings.nickelIIChloride,
    BLLSymbols.NICKEL_II_CHLORIDE,
    MolarAbsorptivityData.NICKEL_II_CHLORIDE,
    new Range( 0, 0.350, 0.100 ),
    ConcentrationTransform.mM,
    new ColorRange( new Color( 234, 244, 234 ), new Color( 0, 128, 0 ) )
  );

  BeersLawSolution.COPPER_SULFATE = new BeersLawSolution(
    strings.copperSulfate,
    BLLSymbols.COPPER_SULFATE,
    MolarAbsorptivityData.COPPER_SULFATE,
    new Range( 0, 0.200, 0.100 ),
    ConcentrationTransform.mM,
    new ColorRange( new Color( 222, 238, 255 ), new Color( 30, 144, 255 ) )
  );

  BeersLawSolution.POTASSIUM_PERMANGANATE = new BeersLawSolution(
    strings.potassiumPermanganate,
    BLLSymbols.POTASSIUM_PERMANGANATE,
    MolarAbsorptivityData.POTASSIUM_PERMANGANATE,
    new Range( 0, 0.000800, 0.000100 ),
    ConcentrationTransform.uM,
    new ColorRange( new Color( 255, 235, 255 ), new Color( 255, 0, 255 ) ),
    new Color( 80, 0, 120 )  // has a special saturated color
  );

  return BeersLawSolution;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * A cuvette is a small tube of circular or square cross section, sealed at one end,
 * made of plastic, glass, or fused quartz (for UV light) and designed to hold samples
 * for spectroscopic experiments.
 * <p>
 * In this case, the cuvette is the vessel that holds the solution.
 * It has a fixed height, but a variable width, making it possible to change
 * the path length. Location is fixed.  Origin is at the upper-left corner.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/Cuvette',['require','AXON/Property'], function( require ) {
  

  // imports
  var Property = require( 'AXON/Property' );

  /**
   * @param {Vector2} location fixed location, cm
   * @param {Range} widthRange variable width, cm
   * @param {Number} height fixed height, cm
   * @constructor
   */
  function Cuvette( location, widthRange, height ) {
    this.location = location;
    this.widthRange = widthRange;
    this.width = new Property( widthRange.defaultValue );
    this.height = height;
  }

  Cuvette.prototype = {
    reset: function() {
      this.width.reset();
    }
  };

  return Cuvette;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model of a simple light.
 * Origin is at the center of the lens.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/Light',['require','AXON/Property'], function( require ) {
  

  // imports
  var Property = require( 'AXON/Property' );

  /**
   * @param {Vector2} location cm
   * @param {Boolean} on
   * @param {Number} lensDiameter cm
   * @param {Property<BeersLawSolution>} solution
   * @constructor
   */
  function Light( location, on, lensDiameter, solution ) {

    var thisLight = this;

    thisLight.location = location;
    thisLight.on = new Property( on );
    thisLight.wavelength = new Property( solution.get().molarAbsorptivityData.lambdaMax ); // nm
    thisLight.lensDiameter = lensDiameter;

    // when the solution changes, set the light to the solution's lambdaMax wavelength
    solution.link( function( solution ) {
      thisLight.wavelength.set( solution.molarAbsorptivityData.lambdaMax );
    } );
  }

  Light.prototype = {

    reset: function() {
      this.on.reset();
    },

    getMinY: function() {
      return this.location.y - ( this.lensDiameter / 2 );
    },

    getMaxY: function() {
      return this.location.y + ( this.lensDiameter / 2 );
    }
  };

  return Light;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Ruler model, to take advantage of location reset.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/Ruler',['require','ASSERT/assert','PHET_CORE/inherit','BEERS_LAW_LAB/common/model/Movable'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Movable = require( 'BEERS_LAW_LAB/common/model/Movable' );

  /**
   * @param {Number} length cm
   * @param {Number} insets cm, the horizontal insets at the ends of the ruler
   * @param {Number} height cm
   * @param {Vector2} location
   * @param {Bounds2} dragBounds
   * @constructor
   */
  function Ruler( length, insets, height, location, dragBounds ) {

    Movable.call( this, location, dragBounds );

    this.length = length;
    this.insets = insets;
    this.height = height;
  }

  inherit( Movable, Ruler );

  return Ruler;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Model container for the 'Beer's Law' screen.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/model/BeersLawModel',['require','BEERS_LAW_LAB/beerslaw/model/Absorbance','BEERS_LAW_LAB/beerslaw/model/ATDetector','BEERS_LAW_LAB/beerslaw/model/Beam','BEERS_LAW_LAB/beerslaw/model/BeersLawSolution','DOT/Bounds2','BEERS_LAW_LAB/beerslaw/model/Cuvette','BEERS_LAW_LAB/beerslaw/model/Light','AXON/Property','DOT/Range','BEERS_LAW_LAB/beerslaw/model/Ruler','DOT/Vector2'], function( require ) {
  

  // imports
  var Absorbance = require( 'BEERS_LAW_LAB/beerslaw/model/Absorbance' );
  var ATDetector = require( 'BEERS_LAW_LAB/beerslaw/model/ATDetector' );
  var Beam = require( 'BEERS_LAW_LAB/beerslaw/model/Beam' );
  var BeersLawSolution = require( 'BEERS_LAW_LAB/beerslaw/model/BeersLawSolution' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Cuvette = require( 'BEERS_LAW_LAB/beerslaw/model/Cuvette' );
  var Light = require( 'BEERS_LAW_LAB/beerslaw/model/Light' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var Ruler = require( 'BEERS_LAW_LAB/beerslaw/model/Ruler' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function BeersLawModel( mvt ) {

    var thisModel = this;

    // Solutions, in rainbow (ROYGBIV) order.
    thisModel.solutions = [
      BeersLawSolution.DRINK_MIX,
      BeersLawSolution.COBALT_II_NITRATE,
      BeersLawSolution.COBALT_CHLORIDE,
      BeersLawSolution.POTASSIUM_DICHROMATE,
      BeersLawSolution.POTASSIUM_CHROMATE,
      BeersLawSolution.NICKEL_II_CHLORIDE,
      BeersLawSolution.COPPER_SULFATE,
      BeersLawSolution.POTASSIUM_PERMANGANATE
    ];

    thisModel.solution = new Property( thisModel.solutions[0] );

    //NOTE: All locations are relative to the location of the cuvette.
    thisModel.cuvette = new Cuvette( new Vector2( 3.3, 0.5 ), new Range( 0.5, 2.0, 1.0 ), 3 );

    thisModel.light = new Light( new Vector2( thisModel.cuvette.location.x - 1.5, thisModel.cuvette.location.y + ( thisModel.cuvette.height / 2 ) ),
      false, 0.45, thisModel.solution );

    thisModel.ruler = new Ruler( 2.1, 0.1, 0.35,
      new Vector2( thisModel.cuvette.location.x - 2.6, thisModel.cuvette.location.y + 4 ),
      new Bounds2( 0, 0, 6, 5 ) );

    this.absorbance = new Absorbance( thisModel.light, thisModel.solution, thisModel.cuvette );

    this.detector = new ATDetector( new Vector2( thisModel.cuvette.location.x + 3, thisModel.cuvette.location.y - 0.3 ),
      new Bounds2( 0, 0, 7.9, 5.25 ),
      new Vector2( thisModel.cuvette.location.x + 3, thisModel.light.location.y ),
      new Bounds2( 0, 0, 7.9, 5.25 ),
      thisModel.light, thisModel.cuvette, thisModel.absorbance );

    this.beam = new Beam( thisModel.light, thisModel.cuvette, thisModel.detector, thisModel.absorbance, mvt );
  }

  BeersLawModel.prototype = {

    // Resets all model elements
    reset: function() {
      for ( var i = 0; i < this.solutions.length; i++ ) {
        this.solutions[i].reset();
      }
      this.solution.reset();
      this.light.reset();
      this.cuvette.reset();
      this.detector.reset();
      this.ruler.reset();
    },

    /*
     * Moves time forward by the specified amount.
     * @param {Number} deltaSeconds clock time change, in seconds.
     */
    step: function( deltaSeconds ) {
      // do nothing, nothing time-based in this model
    }
  };

  return BeersLawModel;
} );

define("image!BEERS_LAW_LAB/../images/at-detector-body-left.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAACbCAYAAACXkTCwAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYOEB4fSIS2mAAAAz1JREFUaN7t20+I3FQcwPHv771kkknSNF0d/+yyuz1YBamgFhUEV+hJ0KMUvelNsTdR0FPxpgfFQ2/25lmKiojgRe1hhYoH6y6ilGqLuPNndzq7085M8p6H2aUFFcTJ6sHfD15eyCGf/F7eD5KQJ9wU7cGmf+OD05z99jP64y2kYagjgr2dc+vf+AdefpJBsoOda5IXC2TpAURMPUi7v+nveeFxytsEW0S05pe4q3WYueZBjKkJOfHmS0ySEhs3CdOEO4vbuSO7lVZyCFtDJmb98k9+9cIqEgUg04OVc1S+QgAjMnMLTn/8PhJbAHzlKUcj2oMOaaNJZCNiG84+XN9dXEMEfOWQ0uGuTei0r+BcxWa3x/L80uzI1Z1tvAMpPX7iYewod0a0q8t0h55ev0szz2qYwn5343ebmzZzKKK3uUH56y+z3Xj+hVDkf4oE+3XiNM45vvAIRw7M14+EQcQTyys8u7iCMQZrbf3Ia/c/z735IlEUEUURjUajXuT44cc4WiyTpilZlpEkCXEc14s8s7hCmqYURUGe5xRFgbW2vtk1l7a4JTlIlmXkeU6r1cIYQ1VV9SFxEGGMIYoikiQhDEOcc1rxiiiiiCKKKKKIIor818hffVGqDWnvtKevbkFAo9HAWksYhvUik3LEVxsXKMuSqqqmD+FxTJZl9Q7XmbWP2NraYjAYMBwOARCRepHe9gYnv3yX9Us/0ul06Pf7jMdj7PzDd5/q7HSRyCCBQcLdPjCYMMCPHK6s/j407PLJpVWuXhtSuBg3KpFjJ5/y67/9gMkDJA6QpkUii8SWoBnh+hPK66N/nN3C3NL+18mV3s9a8YoooogiiiiiiCKKKKKIIooooogiiiiiiCKKKKKIIooooogiiiiiiCKKKDJT3Fga4v/YV9cneF8ikZ0V8VA6fDntGct0yaEDb6p6Mnn06EOsXfweGRswgvdA5fHWIbam4Tp25D7eG1W40GCQ6eK8wIEVvEgtiAAsPP2g7233kNgiDYsEAsZMp4XUhHz49ef+xKvPTX8e20OsQJ2ZALx99ox//Z1TUyA0yH4gexm9+NYrdNttpMYK+tNLPbd23n96/gu2tvu1IL8DGk8OZT1KPXQAAAAASUVORK5CYII=';
return img;});

define("image!BEERS_LAW_LAB/../images/at-detector-body-center.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAACbCAYAAACXkTCwAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYOEB4BsouL+wAAAQ9JREFUaN7t2kFKA0EQheHqSUPPQnAx4CKIa0FEV7lHVjlAvIC38EJzFiOuAhEZhmwCkxDS6fIUbwjxrwt8VHV1QxUdbt9f3cQRp8+PJkdmDy965P7mTo9UIeiR3XHQI7/DVo+sfr71yPrrU45UNkKA/FMktG0rfep3+WCxrmtpFsmTxaZppIi7W+j7XlquUorFlJK2s6pK312lFC4jCAgICAgICAgIiHScyznLN3fB3fXrwZyzvFyx6zr5EBSePubScrm7xc12zT0BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBuWgkhjTRI6b/3mXRh5MeKfvzCJkcx0BGyeSQr+Tg3xZLOfIHIEpQWr10t0cAAAAASUVORK5CYII=';
return img;});

define("image!BEERS_LAW_LAB/../images/at-detector-body-right.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAACbCAYAAACXkTCwAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYOEB8Uxk1eUQAAA0FJREFUaN7t28FrHFUcwPHvzL7NJrtNstl1TQhqPVoQDBZyqRaxIL1YxIMUvOi5KNSD0B6kehCkV6FF0JuIeBG8iAQPpSD5A0qstdBE0tTE7GZ3s5nZ3fd+Pw+7CnoQSl5E8PdgGGYO78Pv/eYN8xv4JbMXl5RIQwfC7ESVV5Ze4r1XL9CYnksAkhOfnY+CqAr7vS4Hey1CM2P6oMKX73zMqaeeTdzyE89EiUJEaGZtfi7fY1vu0dEeL7//BjvtlrrHjj0aBQkqTBSK9IYZrdY2mgWG5YzXPrqAS5MkTiQKQQNBZHQjgaTkWL21iuv0e1GQPAx5sL/LTvc3fL+PhlGqk8kCbqu3GwVZv79BSzo0dx8g2RC8oEFIEnA/bt45NJB19keTlxM0D+hA0KGCV1TArd9eOzTiJkukcyUk64+So+MDBYWUf2EY8j9F3PXnLvPT/iZ3uvf5fnOVXt6JjiQrKysqIoQQEBG++OUG367fYOj7D7dPZov4rD/ajP2AZgHNPdLxpPV6nbm5OWZmZpiamuL14y9waenNuDlZXFxkfn6eRqNBrVZjenqap6vHefHJ5+MhlUqFer3OwsICtVqNarVKpVLh/OOn4yVexu//NE1pNBoAeO+p5zm1SoNmbyfeIywiFItFyuUypVKJNE2ZdCXbjIYYYoghhhhiiCGG/JeQNE3jI8VikUKhwMTEBM45AHYifGwDuHK5TJqmJOO/RSEEvPfc3L71UNXWPyKFQuHPi4ODA7rdLnt7e3y69k28+sR7j4iQZRntdpvbG3e59MMnNPe34yFbW1vkec5m61e+vnuTr9a+o5e345bYZz9/G4DN5sbR1fFHObm9VgwxxBBDDDHEEEMMMcQQQwwxxBBDDDHEEEMMMcQQQwwxxBBDDDHEEEMMMSTGcEmpcOhJRD2aA3+0X/7t7EbtZ4cbCmg2BAEdjnvnvKBewQtOe8Noy6IBCIp6gYHAIKCDgJODEG/xVceIwkCQcROS034kRAEBZLRMOghoHqhVH8HpEUWig1EU1z64itPMR0U0KAxH0Xx48Qrnls8kURMPoAL1RoNr717l3PKZUXPxW9evROtgrh6b5ezJ05w6cfIvLbi/A/qskoFVx4c+AAAAAElFTkSuQmCC';
return img;});

define("image!BEERS_LAW_LAB/../images/at-detector-probe.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG4AAACrCAYAAACHfqn8AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAJ/1JREFUeNrsfWuMHNd15qnqd0/PTM+D5JB6DS2Kol7RKHYSOZJjOkYgJYFByooMIc4upexqEcDASk42tgMkUAT/yGZ/rK0NFkE2QEQGCRIncUQiPxIHCUzBNqLsJtBQsEVJlqWhRIqcIWeme/r9qLp7zq263bdu3eqpmuke9sywpGL39Ex3V93vfud859xz7zVgJxxP3z+H/+bdn+hxjj+LmQCWTc/m8Sy4vy/AS+fmt/stG9vqav/jjx3loIwm74dMYhZMfJ6I5SEVA3zEu8HbaVoALTzjCNpo0nmfxQDaCGCj7T5a9FjAv53H5wsI7jkO7kvnzt4Arj9AEXOOw1jqkwjOUQ7ESBJS6TQkk0nIpTOQjacRQ3zNTEAyloByq4Z4tCGNz/dlJ4Dhfy27jfi0oNZu4nML1poVaLRaUGnUoN6og1VtAtBZaRG4Z/EPX8HvPT3MzBw+4P7DfXMQj52AXOI4jKZmYSINiXQKxkdyMJkZgz2ZcRhPjsBIIo2gpRCwOCTc00TG2cyGJgIVN2L8b5Br/DU62wgaAddmFgeRwKy2G7DaKMFKvQzLtSIUq2Wol6sAaw2AUnMBAT2NjD01bCAOB3Cfv5f80lPIqGdhPD0L01lIZFOwNzcJM7kJmE6Pw0Qqh1giu2IpziYBVtw0IWaYeCP4n+HcDmMMTHzdNKTbY/x/zkCbMQ5kiwPZ6gBI51qzClfrRbhcXoGl8iqsldbQK9bxbCxAufkivvEk/Onrhd0N3OfvnUUB8TyawqdgbxbwESZy43DT6DQcGJmCyfQojCYyMILmMB1HU4jmMG7GOFCmBJR8IwKc7usG/52B76HDA2bnPQ6YxMiG1YQKAlhu1mC1WYalWgEurC3B5bVrUC9WAK5VCcST6EdfQAAXdhdwBFgcAcunn4J9I2DmUrBndBJmx/ehX8pzdo0mspCNJVFzELtiHCjnip1HamRqcIubQXwmHl0giHUOYPxfDhidBHoMzWgcH6kTmNQJDC876b1kTutoTsvtOuJUhmv1NXhvbRHeLy5CoYiEu4oArtROItLXBcCtBe7Je/JIm+fRHD6HNtABbHwSDo7PwExmAhnmAEamkHyU6bJEANR2/RP3U3hazOKvW7btPOJJjW6DYwoFYAZ0QSOw6LPpkTqE8xjHy3LOuGt+Dfd9Fn4P+cwKN6MVuIp+8L3SIryzegkBLAIslhHA+tdREb2wlSbU2ELQnoN86nnYP5ondTg5PgEfye+HGVR+e9CH5dAkEmAxDhaKDLB5o7UIKFSJ1HhN9EX8UZz4est9pJ+Fuas369C2EFxUjsxmkM1kO8Dl0iOQQUVKZpeASuF3pmLOI71G15DkryW48InxDmRwJtN3kagpuCaUGPjm8vtQKZYAnWIBfeELKGK+vjOAe/KeWXRQL6FJPApTGUjlsnBwcj/cktvDFWIeFWLKZZhgFoFAYNWtFvc5NTqxwer4WG1hz69VoFyvIkANqNVr/Gvq9Xqky0qn0G8mUzykmES/SmcGVSop1Qya6Ew8iSAm+Wv0mHB9K3PNtFCjl6ur8GbhIiwsfwitlTIBeBbDiqcHbT6NgbNsKoMsy+Up/ppGs3jn5M2wFwGbTI3yRiJT5fgUBzAn3nIUXgVBorhsDaX6Kqq7MgJGANm2PZDLHc2OwCQahL14nRMYeowkUlwYidCDFG0yFnetAnCml9s1uFZb4+bzPLJvubDisO9q5QU49frXtxdw5MsSsZdgZuQ4Sfv4SApmJw/AbaN7OWhjySw3U1xkEGCWI8krKATKrTqX5Ks1jK3WCrBWLkGz2dxy/ZRBRu7NT8GByb0YSua4us25J4GY5HGjyX0pWYQi+r/LlRV4Y/UDePfaRbBXULxcLJ2GGrLvz79fGH7gPnf3HLLrZbhldBYDaMiO5eAOZNlNI5Pcl5HpMUSgzM1hkwNWIvlN6q1UgJXiKlSr1aEJdqfQH9+29wD2Qep0IzCOHY8AzLgACvZVkH0kXt5Zuww/uLoAtSKazg+KC7BafwzB62sAH+svaHcdh3zmZZgdmyHQRsfH4L49B+FmjMkItASaGfJjdJM82G1VUWajn6iswntXL8HFxcuwiqC1UFQM01Fr1OHyylVYLK2AhS1mG8DVrVCxqFm5EiV1SuKGMzOVgQKrQSPNk95Pwu35t+D1pTeHD7jP3fUU7Bn5C7h5NE3+bM/kFByZvBUOZCdRTI5w8UE3Sgwjc7jSLMEVdOwfrGJsdPkirJVKYKESHOajiR1qafUaN+N2DK2G4QTubuTIFSh1Tjqz6A/H8L5rRhtK8XYa7eqTcGjiApxbnB8e4J64i+KyP0Spz5PAeyenOWgk9ck3UNaCpLTDshpPKV1cuwoXrlyEldWVoQdMx8ClwjKU0GLEU0keugjHQ6EDjw3dEIMEDgcvhlYkZhyHj0wUEbxXrz9wT9yFIiT3FQqoIZuAPflJDtp0epT7AN5TbUd4FNCBL1L8s3QJriwtXhfR0a+DTGS5WoGl4gqwOBrLWMxN7Dgptrgb3FNMSMq0Bi0oGU36xaNw+8QszC+euX7AEdP2jXRBm5iCO/I38fiMTAUdDdsxjSQ8SHW9e+kClNAsMsZgJxxt9NdXkX1VdAEJZJ9Is/GAH/0euQgKIbLIvKoAzzTmYDZfRJ/36tYD9wT6tL0jfyhAy42NwuGJmxG0MR7AkvmgAJpAW0YBcrGwBO9f+mBLhMfHDszBgdGZzvlh6crAv7NULcNKpQjJdApM0+C5VdNwRItpOGk1skBVhh3ZaJFZfRTN5gUEb0M+b2PhwONHjqMQeRkDaw5aejQLd03fBvvRp2Vd89jg8hiZhsHzpZVFWLp2ta8NdXjqEPzEgfvxew/BzShi75q+HcYx3qLDdsmcNN3RAvyHvFARr+X8tR+hf72Cj+/Av314Dt5afqev15VKJuHIrXhNo9NusjzDTSblV8knXqosw2tLP4LiaoECdcBe9Rj85Q9ODx64zx6Zg/HUt+HW8TyBZqIYOTx9K9ya24OgJbmZcDIKDmjvXn4fyuVyXxrl6OxD8OmDD8PPfeQhnpBOmOTvyfI4gwamPLYD3ddtJgHonlTN0LKBD6D+07vfg39+77vw7YXv9UeqI1CHbpmFgxP7feAVm2W4WF6Gf7/yNjSKFKSvFeBa9VMI3vzggPvskTyk46/Bwfws5JKcbTdN7IXD6NeIaWTTW6gQq1YDG6QE713+YNOgEbN+5cce52BNZXKcRQSWODk47miPeGQScCBhyVwABZAEHtUStd2SlGu1Mvwjgvhnr38T3t4kE2kg99AtB9Ea7ufhEAkUAd5KfQ1+hKz/weJ7YBdrAAtFGqR9AMErDMbH3bcX47SxB3ntRyYOY6NjcGj8AA84yRlT8rXCk6/lTYP20f1z8NWf/TL85k8/Aw/sO4QkT6LvdMxfwj3jAkQZTFM6XWBN6Xem5u/pc1AYYl9Mwr17DsEv3f0ZeAC//1JpES5v0D+S+FotFcFIom9DYeKMA5qcjdRWJFgaYPFUGf6Qh1LzCIYJ3+g/cI8feQ7FyHMwmUHQEhDPpmB2fAamUUFS5pyGXyhOKzQqmwJtP4qJ//nIV+HZnzoBh/IzkMYrTEmAxQVgEmjU6KYClHoaMoig/Cydcfd7bsN7+8ydj8DczBz8++Vz2K7lDYMXSyb4SIQAj5hHB8V6K60yT66juTiC7qcI37/6av+A++yRWRhL/QVKfyqpAjSXsG98Cm5Gv0ZqSWRE1pq1Tfm0Zz56Av7Xo1+FOyZnOLt41Z3MLpVNZjBQQaDJJtWQQDTU97nfeysC+OS9j3PzSgBuFLxMNgvJRMJlHZVemNycEgOX6kVgJld0D6Ib+ga8ca3QH+Du2/sy6uoj3K+hiUxnMzA7to9n+Z0EK8r+Zh0+XFmC1UJhQ37sD37hv8Njd/4s+krgLEuq7DK9fs00FGEischY7wQ/iGYQgIYD4E/cNAcP3/YQvL54HpZrK5HBK1TWYDQ3Csl4gocKxDrD9YU1G+O7ZpVuMo0NOYchwqnNA/dZNJHT2V+jMjkCDVJxNGfTPMgmtjXdesUra8s8GxL1+MXDj8D/+LnfgUMTMx3AEpLfiRl+MUKNHJMbXQYBgsHrKDIN8zwCB/yfRY/7Ribh07d/CpYqKyhefhTpPimtV6pXID867hQ8mUan1oVYt9ws8WAej1nUEUVk3asbB+7YYZL8L8O+XJoUJJnIdCYNt4zu4VVXFGTTOBopyA8+vBg5G/LFj38Bfv3jz6BkTjp+LNYFLK5Tj7LJA0lRgsYU6k7pb4SmVgEE6W/V39N7qfj2k7MP42MO/uXi/4uYpG5CG9uMBmxjbnAuUmRN1uaijn9Jrf0g9uQ/QvDqGwPu7unfg5ncUbSJHDQ6Z3JTMImxCdnpNg8q63Dhw+gZkd/55Jfh8/d+hpM46foyDphQfxowTE0jy0BGiYHEZzAJJMFGz9+oALtsv2/f3ajVZuBsxNiPMizk7zJcrLilhu4FLKMA4qwzjDTazjQKlW9FB+7YYYzVUn8Je7IgBAmlc0iQ0JgTDWNU0UR+uLzEc49RQXvirkd4XxCmMRYFNKkBNzMSbCixng48kL/D8L7v8PShDYFXrlVhcoyKBGKOUKFSCMMpRuKs40MQyLrbJ04FCZVg4O7Z8zU0kXM8Zks7vm16JM+DSXKoTbTZNC61eHUxkokk0H6JQHNVo2oWPcJANm/gBdHs49i90QM8HwslMOl1ElbTCN4rF8KDR6yiko2x7Cj3bzwpDU4l2QoC1263nS9Ya+aRdWfCA8fZljwJU9kOaGTPbkIzSYlSAorqLC4tXY40NPPsg1+AX0bzyD9SVo2GHziZZaYqRAaQJDaMcK97/KMLJDEvjT7v1Qg+j8b0crkcvi/lFPzycg4GFasBpUbVseG1No0inILzftbFAnzb19BEOmxLOcBRPSIpSZG2WS4X+CBo2OMX7ngEvvjgMx2f5lGNpjd9pTOR8uOgDhkU4fuYBBLrYXPv2Xs3fLB2BX4YUm1S52+hsBvPjTmhAZX+Gc6QENVsMsrB0QUUG3l4w886U6skM4njMJLoUgJbiyZcOFVNjJfPRQHtDjQnv/HTX3BMowSYzC5D8TmqGoQBg9ZpECPAPIImAyMyLYYTe34J75HuNeyxulaENQwRaPiLkhhUaE/1nGOZnPPB1MtHk8d5jnhd4GjWTC6Z5xMFReSLVznq5iNpNHutWopUgPrbP/MlmM54s/mm0hAG+BvKMAJExBaAZ6hAqvGd5GtFZ5zO5uD5T34p0nd9eO0KzzpR2ovYRlVjNNmFNxaZprFUHr/wqfWBS8ae7bDNdTzpeLITbNOQTZTsyH/68RM8cZsw/TGZoXH4qnozFDC34tB1HN81SRkbU2LefXsP8XuOwrpSvcoVJVWO0aDraBJJEnfN0whX9M/2Bu7Y4Tn8o1mOtJROF7WQzI1DwrJtf24GnsGbEB/nUYsquwLMk5r12ErwPNcgg2n4g3rRXBTe/NrHTvB7j8Q6DK3atu2OHCRgJJV1PpR6/Ehylo+DBgJnGCeoSsvxay5FDKNT9EN2mCqLwx7/FVVkpw/IJtHwBr2DGaYfjMoEpbOZkikX4JG/e+7jXwj9XWvlMtRaDc46Ovhs2mS660RJJMaME8HAJczjXKsrLU2THnjKhuaLhcz803jWp2Yf6ggRIyglFSDv5b+7Hoeh6TSGDlwl7SYSCZ8++BBvg1B5TBpcLRWdaWS2xVlHmqIzWOgo++N64MhMptBMJrxjJXE3uqdMCdnjsMevPnDC0Tdy9h621zIPPt8bAKxn1MEFkEzmf47g664VVnje12JWx1zykj8xPJGJe8ylzLijPJEc8w5eEW2dXmFDpVIJdREzaN8/duB+Z4BTHjKJCt518m++mCuECff5PBoKwjYI6+to9Ydas87dkWP84nwIqIMFiRTGjvqBi5vHnIIO09OFSFHSUW3WQmdJPnfP4yL88/TG9Rpm2EotjYg+2JOSA4cokRRmaY3X7IgjJYAjMiX58MkxP3Ax8yiQBJW/uTPfug3VWi3SGFtMMh3qUMm6AK7X1beKaSyYeSzg8mTwqA1+Bv182KNSq/KQgEIuUcruCRYTMYVx5N9kzS5p8xQVt1K2JCRwn7jtIYwZcx1br/XymsZhSgt1XmfX307K19DretROSvefx7agNgk15FMpcZdkuwsPUNt7bC8JR8JK+i4HOLVAUWrssLHbJ2592DNYaQSkZ4LoxjTn9fRt6nWsd02GhnWfuO3h8KEBxsmsYwRNby9Ixx2sOm1qGvd31KQmIqZhhrDTd398//2e/KO2B2sagWkYeD3B05lt1YQz5jefqt8j4D6KbRL2qDcaXFnaoiHkIXinEOd+Gbg53/CyCxoNmoYVJYcmD/HyOlOX8WDeG2UKy3y9WZgodn3MJWPdU1ybrbleYL1VJ7XBTdgm1DahgCNlSeZSfKon2uehgYdxs1016adK2LKEO6Zu99Z0hOzVNgs2l/Z1YJ3vu5nGMoQQLLLJpLYJczSQJM7aLZY/683rEc3ZLnAGAtdjhDIK44x1fAVIjaFjm6dnM2+9/1CwjXU7m2ze2TphxR0hGSeW/2Bu7NwVC67SjwvgaLQ7bvaU6WH9mxiLMnrckGpe5IbQNYzc67cCPNud2WNJ8wy0JlLtfD1MuyG1TagUGLODewARDDEjDGfXS2WEZdz+3D6tmfQIDeY3kTrwmAKYDYNnnq1MCpGvI4htNlvfqvC2Gd0X+jqqNWfFCZF09hxxbiRn42FSBGEZR8JE57zlpe2Yoe+9hsZHUKOYhgSW0WVrv2tP5Bk8NuvOqbOVTqV2NhZGkbppwCg+1lmWse2nrxuqmar073fWgck+Q+fjNA2jmxJla5jXD/Z1wAoBGlOuXQbRZr1DmCitKxZ+oyxKUH5Nzzjp6rRvDgtYDxPSmcdmdEkpM8w0umxVmWcoLNUND9maOXJBapaxrk/rBVoQOEzpoHLct5G+JVYKpFoUv1TtAjcXmJjD15rt9sbYJrWq50ZdcylbPwGMIYGqA08FjL+H+UfS6XdVN4KhWl6jx3XaGrYHgQYaS6ELwjuvKb/LJXNQDjFdixIeRBgCz5+BNyQfxwJsATizSaICxjQfJRRizOg2RMzQsMz9Yx947jVbrAsYk+JSASAzggc/mcIEFmCWfdcdZM5lJjJ9aLORBEI8HudLZGk/wHmeJ+DmvQGJtyuRkyTwwgoUHbOYRrAYGtBk8Jj0mgBHZqM8tidel1+jyjYw9L5Vjb+YRtFCAJBq/GYz/2cHmdVyhMmRtCqfVpI7MMzH9d61exekbGip+CjlePzjKORQwBM3GdP4N9VEyiZUvNdQ2MeY5N+E/3M/0IDua74OBfo0VlB6KygM0IEks49t0M+ZyTjYVebPBthdYsX92V6v3bDdFcWjZtPBNY2m20mEGWPgZVYvE2nIgEEXKFMCjcmgQRdco5dM75H1CMqI6GI3uwd7BTk2MjxlB2YheGwmhQO09IDNtBEoFcAS48Ic76y84+kcupEA1sP8qP4kSG5bzL8EBlNCBUtZGsPSvK5+r9WLRRAcz2nZpnDghyvhVnFIp9O8zb03LjWq1WXcfPcu/D6Oav0MI9yM43Kz4r1owR4IZp36qJpIS1GcTIpeLKaEoMwf0OuGXHT+TpftkDsSBKhPrT9j/k5JbRPe1WiCYIFP29kryIQzbxe0eSX3imhKUFjGvXZ53ucnBNNBSWH1Umu655ZGFMg9WmaV+E75ZBo2qmk1+dotpg8HdNfuMY9KwkB8PrVNWMZ5vlT+IHGTiJnpOqMFn6mUWqZthDPQV8pX/Kamh2mCgEedidQC6L9U3/3qvptpwNcBBor5s4LiPIUUHtfkPqe2CXNQxThPejBNT3AubqHr4xj+0LaVu+w+2iYLybhzOn2jjXl69V65kdQoBcDPGpXN651ME4epgMlS32bB1qFjqALG8ewO48IttUHWjdyTL7dnu8sg2bYMHLziA066slZIc0m9ik5fmkhj2mzN8kw6/2BrmKQyztKZv4BTJ160DFY6EOsBGmMBfd793WW3XcICR7N3fB8qtlKz2St+xgl1qdgbmpAQ3s+d05oiFmAugxjXEzBNo8M6ZlL9btD5Xeb/HFCS5LqhH5v1SIpHYBuFXXW7pb8RwoZWjbNkU0nKkoDTsY4LFAviiXioL//u+9/txHDiUWTfWYBAYUEDlxqBYPUAUWcGdWpP9/egCzUguKOpIxasx3ieaJMwbKMydA/L5BtvWg5WHeDOvD2PiBYc1um7ajukn/vOhe/x1I6tcdieG2XhfVwvE6myiCnJBs8JeoZaTK8idSZbJ74ZC2g2N831nZAT+0lR0npoXX/mPlouqVpWgWMFcskjg7Mdc2lpAIyHTzb//Q+/1WGbzs8Fgafzf73CAOjht3SJYF1AznqYTF1QrrvuXqZStEUoRZmIOSMCsnkUF0vbh1rsbMesSjLqFU5FmXWdN9pQbdUhm82GuoC//sE3fVLaE1MxvWNnGgEDGtWn9aEBRUc6EKFHWKDLoliaBIbPDWlMpdwWocbhYqAnjtjX1bJf8QNHe4IKP2d5QaM32/R6MlwGhRSU6Gm+tBTTgyePROvSW0E+qKfvYv4cra6TBJlenYm01FH4HqKI2iCsmiRS0D5CXZMgtX/L3ZiXMPIBd+btBa5Ymjb4fJ373I6wLOlLr53yVE3peqiasAEIkNQQ3KjWeowKGRboBIoNejOodjpLw0K5DUIllhMG2JaGNI5vI/AWOEYaxtFVnIamu+Wy7BT5aUG91Qxdm+JhncYEBsZA4I/rdOmtMD5MPdfzfyoAjAWbSh1osnqOwjZq07ppec2keGx1zKRnwW1VcbzI/5AQFgsVi8Cv5X5oKhaJdaSqtGZH4xssFixOWI+A3ApKa63DNEuTnuqVKLBBa4h890T3HIVtfNTX2ZjOaee2dNLP9baDjXR4UXhruQCHJ49DzJzxzEyUV0QjP9cMV0BEN9C0mvBTN/+kpyZJt36kWmcgTxb0yHgDfEOIYATUcEJw5ZV2MgcElzCsp35lE/lH//bH8H8vRVgSkRZwFb7MMYvOSe1MxTPV1jycfvv3ezHOQbbWdujZsr2sEwF6MjzrSFXNXznnTxUF5RA1QsVeR0SwXp+1jjixAz7XZsGiqRdodK9RlGTHgul8mwAuZr4Iocr9jh1ehUwi76x2nnDWf6C5WQn3kYArh198jYpB/+T4/+FVTgABi6xpFg+Vl4LykFJTDKsuKhO2hFBXSBQ0lMMUFaz6TLIwv3r6v4T3bXSMp5w3N1yyiEciD7VxsV6Ab7454UuPBXzcixxtOoW/k3sCixaQ04383nd+31ce4MtC6Hyf6sd0sR+sP6Tjy51rPsdWmddjZEGXpKZ7jASamJMYpCbJtzF4UfdWvc07MkUjol9xPjzmX8maujW93gq/V+n7xQ94jxT+junYsc7iImr5Ohi95T/A+sW5cn2lruTA1vg6Nf6k4w/+9X9HypLwg1ZSoMPj21zCEOMqLWrnx+D7V+vhgHtruY7gzeIVzskr6HVm9Yi1NywWqYTpjavnudkUM1eYIkCglyFX5xwY+gIlFiRgwJvF0SagfVVw/vylml8VKT4SJNFKuQy+154Up3UfG65va1kn4a/PfyM84xzW0fjMc7xV1R0aBHj0ejvaDsHfff97HvAC2derbiQIGCNcAK6dbx7AuqDxQxk02Q2EPrLuUhgqaMS2uuVqCOOx6Ev7UmhwZOooXuUsN4txaX8TYS5pqaJW9K2dg8DzsM/QAwW62T7G+qZRBVRX+aYTJ6oYgX6AZrgLzvhCAMlMNtpnUZS8GPQRvXX9kakiXvWTHKyEZC7llTYNo1MyFhU82efp2KcCZWiYpjOZYc4gJcmYNxkOGpYJnxbZPMohgNAILSns4sE2/lxq0Bd+EdkWuBnu+uL52OH3+PTV8bRD70wcOsuXu2s1cye6wYPWAPmtT3y5EyqobiAoDFCXSQxzU2ydAtmguQVqUoFY9p0Lm9iyTIQA3Cy2nUcKA4hpJR4CLCDbDvb6iPUj6SNTBt7Bo52V3MRSfIa0AY0NG14agdTmP7/7bW421cl/TOP/wogT2XxqR7LVwp4e8wvkg4Lr//atr3CRteGDL9mb6IoSWZwQ2ypN+vkFOL/cc6ePMIyj9YBXObPG3W1aiHliaw5in7M7BWz2eOKex+HpB05o2ReaZZq0GdPQLHBqVEDqjnKPkTIivdJb1Pk5y6SAu+4G3IU66YiJ9faSW59xIjRgMOdsx+EuHRWXViJKbkyk6MKFM2/+HUxmJgMnuzMIzl0G/a1WtLBwS3GQAPnNf/xKJ223qaMjSljXv7UlUUIupxEcAkRjnMO6o/jvtznTxlIOy+h50t31gX7uFrP05SCzSez7+TsegetxEGDEskiZkDCihNpNxzYCbbVG7fgp+JvzZ/sDnE6kZN0cJgfPzWNW+r+vNwFI4NEZZQL8Rg4xhhhpLC2qKKFDBk0WJWuNBQTtYCjyRgCOgvGv8cRzzk0+Z13AqCfRz9XWQNezoAVwCEBSov0CkQAihUhgvbPyzuB6BYkSslZCQQpFKbIk5NtqrS/C37719X4D54gUMouySJHNJR19EClhmUjLLBGYYu3juZnei50JP0UTMAgk2pVjIMwKyksmYl3gBNu4KEHgVmoE7kTYDW6jrZFx7PBLqHie4pSnPFs23h3mSUUf7tlVB22cKHQAZ5o0fLPWoNjtJLz81tOhU50Rv/4MnxHZUkrIxFCEGE24cXgP6tBiqq2lVIu3XCBj5plIAjXyRZBIoU0lVHOZdllHF1ht3QBLjd3IxwnfJpRk3RUlhRqKkjcPRvlIcwOXcdpTxmDZ3nKnuHkDKDV249klxTqJQtcmr1A+HfVjN9LKp6SZI/6yJxatJmXnm0mzaybV2Tdtt9DVME4NHjiadMBg3qll10yGJPBu+Dmvf7OZvvDT0Qrz8Ldvzm8F4xzWtS094+gxYQ7HTg/DYCZl4LTzAdipjXz0RoE73ZlBos7jsiXwbphJ/5Qe2b81uJo8vXXAUQ27zeY9s1jVsqgbwDkugykzn2T/Rm34N+cXtpJxjrnUCZQOcDf8HGecrqadyh15wbF1aqMfvRngzvoYJ7OO97hdzLqEu3+XrrDT6pjJs1sPHKlLW7PMhnzu5rBA3LtuKpGjJhc2oib7wThnWpZgna7sdzcH43FTXwbdnRZ8ejMfv9mWfYX3Ho+yhC6IsV0aFohqON1sE8sNA6jtNhNpbPoiHz/CYCIDkEs4IwXuhu+d3CXlLfs4Mr4tDhrmElXK6hBOCdtjuQrwV28Y15Nx1IsckaKbvcF2aVggFLU2Y8Jdy9lNk7oPwL3SmQLrW30Gdqefi0kro6pLqzetzrJO1xc4ERb4Fu1yz93m58RcC+1KqK4woTbb7Nf05WI/dzfjI7w0PJ9yR8XFyDg9Ukl1H8r3tk38RrUlFGQ32926ElE3eXXz/q1fjHP8nKVZbU2Yi92URencK/MvxMIzTJv3b/0Ebt6z8p66MEhsF5nKzsYK4J/U7oi4+SECzj7nm38r+zlzFwHXEWOaZfecadjnhgc4Bgv6Vc12obI05dko0opuli2EyUJfNFDfLvjJe5xAfCTu1lmKgNytdi42Is9e3bbCRF5FQZTiUe3kYhngG28Yw8M4x88tBO49xqC/m70NcygAoLQBiH0zHcvUL2L37aJpeWB1DSd5uYLdoCyFS2DKHKGOQLGHEDhSluq8ATlzsCuCb4ltHj/fKRieH0bGFf1rMUmg7QaBEjOVTutbULo4fMBZLuN6rni2C4+OquT7yQwh4wAKnfkDOlO5G4LwoHvsZpMKwwictIG2Znny3RCEB23Iaruqso9t0D/gzrx9ls/kYUGLUuwy86hzFy+/dXY4GRekHnezj5NNZT/J3XdTqVuGh7Gdby5V1cwCtjEeWsYx0O/a3st576QYLrBtYIiBo5FdJhn33R4SyHGcsflR70ECd+PQuhA25IxjcEOIbFXkMTDTcOPws264GcduAMe2G+NuxG49OvN2CAfCBOU3mDdkjNMBJV6K7WARq94b204+Ti2EUUHcyZkT09CDJqyQ0b+yhUEAd6Fz5WyX+jqmuWlim2FcGHJTCf7lxXelf1OqAIY85bW7xci2DcDtgOKg3QambkhyyMWJZpvgXWgyWcDm5NvGVDJlF/bd7uuGOhzoxbSdPB6n3ptnahVzCoaHGjhbM0wvWLiTZ6Yaht9MMs/9DzVw/uqu3ebjdP5sAG5iQOJEKdHblcpS2UF3yIEraPc9EayL7+RioQBTKWoqqW2GFjhnfS/NigOCcbvBx4F+H2tqm21jKnV+bicKFB9o4N+nc/h9nDytmPmzKTvRXMYNf5bE4+f7GwoMinEL3lk7Cng7cWhH3FPwpuLbADiakWLZyu7q0tonO3EwNWZ63YS6zLHFCsMPHMA57VK/ohfuxAmOcTN43+k+LpExaOAWOqujyz1PTL+K7VAfx1jwphDbwlTSPGdL3fwH5HnQO4t1cWUnD3UfBmdtk/l+f+1guv9jdzKYzLgbBErblPE1T+LA59FVdsjGSWLhuabVPcWionSPfVp0bSsYR2bjrG9pe7HuPj3uJMbRMiBivU5L2p6tu2j22YEI2cEA5+6901ZupC0tSJraAeue0D0QlyymnLa0U5U9v32Aow0Cmy5wbebtjWL75HR8e2dR6Nqz7kbs4hT3J9hGJpPBmYF8/cBu7PjhVRhN5fHs7l5Mp9gMN+nuI05rXG23kQMCbSzpWI6m5XREARQBV7ecHZpXagX45psT24lxdFMnO866zbw9U7xOPZNWT99OQTn5Z9qZWex3KkAT9yMsCokTyz45sGTNAG/xRc/WyW3pplp2d0l3ep51t+wc5nSY6ZrGkYS383HQ2lKntLub1jJsg0ElawZ2o28tF+DOqVm8+LlOrANSFt1WNgqi34udjSlIp4YS8dH1YhZZArFVdsLdB64pOp97ykwTYNLay432STjz9qlBXd5gu/ixw7P4Da9BJpHnvZWAoTUd6aSGMV2AYu7P4jUBnDgBuuDSQb3cWOcOLDehbQSa8u6j2BvBljI7YqlCnjyw9fubyqFO0wWONsgoNQr4+gN8u7Ztx7gu6xp4g4/yRpdXl5NHyUVDtNxTPG+7mYe27W0o9TOY1A8NybT1AlV+nyX54JZk7oRZbynXJq6lzbp/33TNI4kSi/0WgvYPA9VHW2J2aON303iqs6w9mZ0ghhnQZZYBXcYJ0ylftRxORL0TtRZGLbXgHQo0++MI825LYsRVlMS2tk0m8ulBN+nWqQECz0DwklJoEHPNpAc4w5FMhgSiDiidkAkbF2rn8Cmr29ruD6o/lmslZZVca9FrWwLa1gLngPe7+O/zHCzu61zm+YDrAZqhXvVmboF5n6qldbay3qStbE4rQKONfhm8gKD97paFkluu1kiwALyE51GPMIkK3kaY1ot5qukMAs2yvek7Z7uVpwcpRIYDOC+Az1KOBc9ZD2ACNNWv9Yrzwvi7MDWelvQ7MQxlqfsp8Krk0zxW3WLArj9wXhDnOAMB7ucgOs/dqzPWv1IZ4DCHzcKt9Nf9G2IVAXSOP+9zqd32BS4Y0KPSTwRuPsS7bnPBl4+C2+jrHfR3XVBoDc4hPf6/AAMAZZMHE8V8a9UAAAAASUVORK5CYII=';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Detector for absorbance (A) and percent transmittance (%T).
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/ATDetectorNode',['require','BEERS_LAW_LAB/beerslaw/model/ATDetector','SCENERY/nodes/Image','PHET_CORE/inherit','SCENERY_PHET/MeterBodyNode','BEERS_LAW_LAB/common/view/MovableDragHandler','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY_PHET/PhetFont','SUN/AquaRadioButton','KITE/Shape','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text','DOT/Util','DOT/Vector2','image!BEERS_LAW_LAB/../images/at-detector-body-left.png','image!BEERS_LAW_LAB/../images/at-detector-body-center.png','image!BEERS_LAW_LAB/../images/at-detector-body-right.png','image!BEERS_LAW_LAB/../images/at-detector-probe.png'], function( require ) {
  

  // imports
  var ATDetector = require( 'BEERS_LAW_LAB/beerslaw/model/ATDetector' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var MeterBodyNode = require( 'SCENERY_PHET/MeterBodyNode' );
  var MovableDragHandler = require( 'BEERS_LAW_LAB/common/view/MovableDragHandler' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var AquaRadioButton = require( 'SUN/AquaRadioButton' );
  var Shape = require( 'KITE/Shape' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var Vector2 = require( 'DOT/Vector2' );

  // images
  var bodyLeftImage = require( 'image!BEERS_LAW_LAB/../images/at-detector-body-left.png' );
  var bodyCenterImage = require( 'image!BEERS_LAW_LAB/../images/at-detector-body-center.png' );
  var bodyRightImage = require( 'image!BEERS_LAW_LAB/../images/at-detector-body-right.png' );
  var probeImage = require( 'image!BEERS_LAW_LAB/../images/at-detector-probe.png' );

  // constants
  var BUTTONS_X_MARGIN = 25; // specific to image files
  var BUTTONS_Y_OFFSET = 66; // specific to image files
  var VALUE_X_MARGIN = 30; // specific to image files
  var VALUE_CENTER_Y = 24; // specific to image files
  var ABSORBANCE_DECIMAL_PLACES = 2;
  var TRANSMITTANCE_DECIMAL_PLACES = 2;
  var NO_VALUE = '-';
  var PROBE_CENTER_Y_OFFSET = 55; // specific to image file

  /**
   * The body of the detector, where A and T values are displayed.
   * Note that while the body is a Movable, we have currently decided not to allow it to be moved,
   * so it has no drag handler
   * @param {ATDetector} detector
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function BodyNode( detector, mvt ) {

    var thisNode = this;
    Node.call( thisNode );

    // buttons for changing the detector 'mode'
    var textOptions = { font: new PhetFont( 18 ), fill: 'white' };
    var transmittanceButton = new AquaRadioButton( detector.mode, ATDetector.Mode.TRANSMITTANCE, new Text( strings.transmittance, textOptions ) );
    var absorbanceButton = new AquaRadioButton( detector.mode, ATDetector.Mode.ABSORBANCE, new Text( strings.absorbance, textOptions ) );

    // group the buttons
    var buttonGroup = new Node();
    buttonGroup.addChild( transmittanceButton );
    buttonGroup.addChild( absorbanceButton );
    absorbanceButton.x = transmittanceButton.x;
    absorbanceButton.top = transmittanceButton.bottom + 6;

    // value display
    var maxValue = 100;
    var valueNode = new Text( maxValue.toFixed( ABSORBANCE_DECIMAL_PLACES ), { font: new PhetFont( 24 ) } );

    // background image, sized to fit
    var bodyWidth = Math.max( buttonGroup.width, valueNode.width ) + ( 2 * BUTTONS_X_MARGIN );
    var backgroundNode = new MeterBodyNode( bodyWidth, bodyLeftImage, bodyCenterImage, bodyRightImage );

    // rendering order
    thisNode.addChild( backgroundNode );
    thisNode.addChild( buttonGroup );
    thisNode.addChild( valueNode );

    // layout
    buttonGroup.left = BUTTONS_X_MARGIN;
    buttonGroup.top = BUTTONS_Y_OFFSET;
    valueNode.x = VALUE_X_MARGIN;
    valueNode.top = VALUE_CENTER_Y;

    // body location
    detector.body.location.link( function( location ) {
      thisNode.translation = mvt.modelToViewPosition( location );
    } );

    // update the value display
    var valueUpdater = function() {
      var value = detector.value.get();
      if ( isNaN( value ) ) {
        valueNode.text = NO_VALUE;
        valueNode.centerX = backgroundNode.centerX;
      }
      else {
        if ( detector.mode.get() === ATDetector.Mode.TRANSMITTANCE ) {
          valueNode.text = StringUtils.format( strings.pattern_0percent, value.toFixed( TRANSMITTANCE_DECIMAL_PLACES ) );
        }
        else {
          valueNode.text = value.toFixed( ABSORBANCE_DECIMAL_PLACES );
        }
        valueNode.right = backgroundNode.right - VALUE_X_MARGIN; // right justified
      }
    };
    detector.value.link( valueUpdater );
    detector.mode.link( valueUpdater );
  }

  inherit( Node, BodyNode );

  /**
   * The probe portion of the detector.
   * @param {Movable} probe
   * @param {Light} light
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function ProbeNode( probe, light, mvt ) {

    var thisNode = this;
    Node.call( thisNode );

    var imageNode = new Image( probeImage );
    thisNode.addChild( imageNode );
    imageNode.x = -imageNode.width / 2;
    imageNode.y = -PROBE_CENTER_Y_OFFSET;

    // location
    probe.location.link( function( location ) {
      thisNode.translation = mvt.modelToViewPosition( location );
    } );

    // interactivity
    thisNode.cursor = 'pointer';
    thisNode.addInputListener( new MovableDragHandler( probe, mvt, {
      endDrag: function() {
        // If the light is on and the probe is close enough to the beam...
        if ( light.on.get() && ( probe.location.get().x >= light.location.x ) && ( Math.abs( probe.location.get().y - light.location.y ) <= 0.5 * light.lensDiameter ) ) {
          // ... snap the probe to the center of beam.
          probe.location.set( new Vector2( probe.location.get().x, light.location.y ) );
        }
      }
    } ) );

    // touch area
    var dx = 0.25 * imageNode.width;
    var dy = 0 * imageNode.height;
    thisNode.touchArea = Shape.rectangle( imageNode.x - dx, imageNode.y - dy, imageNode.width + dx + dx, imageNode.height + dy + dy );
  }

  inherit( Node, ProbeNode );

  /**
   * Wire that connects the body and probe.
   * @param {Movable} body
   * @param {Movable} probe
   * @param {Node} bodyNode
   * @param {Node} probeNode
   * @constructor
   */
  function WireNode( body, probe, bodyNode, probeNode ) {

    var thisNode = this;
    Path.call( this, new Shape(), {
      stroke: 'gray',
      lineWidth: 8,
      lineCap: 'square',
      lineJoin: 'round',
      pickable: false
    } );

    var updateCurve = function() {

      // connection points
      var bodyConnectionPoint = new Vector2( bodyNode.centerX, bodyNode.bottom );
      var probeConnectionPoint = new Vector2( probeNode.centerX, probeNode.bottom );

      // control points
      // The y coordinate of the body's control point varies with the x distance between the body and probe.
      var c1Offset = new Vector2( 0, Util.linear( 0, 800, 0, 200, bodyNode.centerX - probeNode.left ) ); // x distance -> y coordinate
      var c2Offset = new Vector2( 50, 150 );
      var c1 = new Vector2( bodyConnectionPoint.x + c1Offset.x, bodyConnectionPoint.y + c1Offset.y );
      var c2 = new Vector2( probeConnectionPoint.x + c2Offset.x, probeConnectionPoint.y + c2Offset.y );

      // cubic curve
      thisNode.shape = new Shape()
        .moveTo( bodyConnectionPoint.x, bodyConnectionPoint.y )
        .cubicCurveTo( c1.x, c1.y, c2.x, c2.y, probeConnectionPoint.x, probeConnectionPoint.y );
    };
    body.location.link( updateCurve );
    probe.location.link( updateCurve );
  }

  inherit( Path, WireNode );

  /**
   * @param {ATDetector} detector
   * @param {Light} light
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function ATDetectorNode( detector, light, mvt ) {

    Node.call( this );

    var bodyNode = new BodyNode( detector, mvt );
    var probeNode = new ProbeNode( detector.probe, light, mvt );
    var wireNode = new WireNode( detector.body, detector.probe, bodyNode, probeNode );

    this.addChild( wireNode );
    this.addChild( bodyNode );
    this.addChild( probeNode );
  }

  inherit( Node, ATDetectorNode );

  return ATDetectorNode;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Representation of light as a beam.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/BeamNode',['require','SCENERY/util/Color','PHET_CORE/inherit','SCENERY/nodes/Path'], function( require ) {
  

  // imports
  var Color = require( 'SCENERY/util/Color' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Path = require( 'SCENERY/nodes/Path' );

  /**
   * @param {Beam} beam
   * @constructor
   */
  function BeamNode( beam ) {

    var thisNode = this;
    Path.call( this, null, { stroke: 'rgba( 192, 192, 192, 0.8 )', lineWidth: 0.5, pickable: false } );

    // shape
    beam.shape.link( function( shape ) {
      thisNode.setShape( shape );
    } );

    // fill
    beam.fill.link( function( fill ) {
      thisNode.fill = fill;
    } );

    // visibility
    beam.visible.link( function( visible ) {
      thisNode.setVisible( visible );
    } );
  }

  inherit( Path, BeamNode );

  return BeamNode;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Visual representation of a ruler.
 * Lots of options, see default options in constructor.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/RulerNode',['require','ASSERT/assert','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY/nodes/Rectangle','KITE/Shape','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'scenery-phet' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var Text = require( 'SCENERY/nodes/Text' );

  /**
   * @param {number} width  distance between left-most and right-most tick, insets will be added to this
   * @param {number} height
   * @param {number} majorTickWidth
   * @param {Array<String>} majorTickLabels
   * @param {String} units
   * @param {object} options
   * @constructor
   */
  function RulerNode( width, height, majorTickWidth, majorTickLabels, units, options ) {

    // default options
    options = _.extend(
      {
        // body of the ruler
        backgroundFill: 'rgb(236, 225, 113)',
        backgroundStroke: 'black',
        backgroundLineWidth: 1,
        insetsWidth: 14, // space between the ends of the ruler and the first and last tick marks
        // major tick options
        majorTickFont: '18px Arial',
        majorTickHeight: ( 0.4 * height ) / 2,
        majorTickStroke: 'black',
        majorTickLineWidth: 1,
        // minor tick options
        minorTickFont: '18px Arial',
        minorTickHeight: ( 0.2 * height ) / 2,
        minorTickStroke: 'black',
        minorTickLineWidth: 1,
        minorTicksPerMajorTick: 0,
        // units options
        unitsFont: '18px Arial',
        unitsMajorTickIndex: 0, // units will be place to the right of this major tick
        unitsSpacing: 3 // horizontal space between the tick label and the units
      }, options );

    // things you're likely to mess up, add more as needed
    assert && assert( Math.floor( width / majorTickWidth ) + 1 === majorTickLabels.length ); // do we have enough major tick labels?
    assert && assert( options.unitsMajorTickIndex < majorTickLabels.length );
    assert && assert( options.majorTickHeight < height / 2 );
    assert && assert( options.minorTickHeight < height / 2 );

    var thisNode = this;
    Node.call( thisNode, options );

    // background
    var backgroundNode = new Rectangle( 0, 0, width + ( 2 * options.insetsWidth ), height,
      { fill: options.backgroundFill,
        stroke: options.backgroundStroke,
        lineWidth: options.backgroundLineWidth } );
    thisNode.addChild( backgroundNode );

    // Lay out tick marks from left to right
    var minorTickWidth = majorTickWidth / ( options.minorTicksPerMajorTick + 1 );
    var x = options.insetsWidth;
    var majorTickIndex = 0;

    var ticksContainerNode = new Node( { pickable: false } );
    this.addChild( ticksContainerNode );

    while ( x < ( width + options.insetsWidth + options.insetsWidth ) ) {

      if ( ( x - options.insetsWidth ) % majorTickWidth === 0 ) {

        // Major tick label
        var majorTickLabel = majorTickLabels[majorTickIndex];
        var majorTickLabelNode = new Text( majorTickLabel, { font: options.majorTickFont } );
        //Clamp and make sure the labels stay within the ruler, especially if the insetsWidth has been set low (or to zero)
        majorTickLabelNode.x = x - ( majorTickLabelNode.width / 2 );
        majorTickLabelNode.centerY = backgroundNode.centerY;

        // Only add the major tick label if the insetsWidth is nonzero, or if it is not an end label
        if ( options.insetsWidth !== 0 || ( majorTickIndex !== 0 ) ) {
          ticksContainerNode.addChild( majorTickLabelNode );
        }

        // Major tick mark
        var majorTickNode = createTickMarkNode( x, height, options.majorTickHeight, options.majorTickStroke, options.majorTickLineWidth );
        ticksContainerNode.addChild( majorTickNode );

        // units label
        if ( majorTickIndex === options.unitsMajorTickIndex ) {
          var unitsNode = new Text( units, { font: options.unitsFont } );
          ticksContainerNode.addChild( unitsNode );
          unitsNode.x = majorTickLabelNode.x + majorTickLabelNode.width + options.unitsSpacing;
          unitsNode.y = majorTickLabelNode.y + majorTickLabelNode.height - unitsNode.height;
        }

        majorTickIndex++;
        x += minorTickWidth;
      }
      else {
        // Minor tick marks
        for ( var k = 1; ( k <= options.minorTicksPerMajorTick ) && ( x < ( width + options.insetsWidth + options.insetsWidth ) ); k++ ) {
          var minorTickNode = createTickMarkNode( x, height, options.minorTickHeight, options.minorTickStroke, options.minorTickLineWidth );
          ticksContainerNode.addChild( minorTickNode );
          x += minorTickWidth;
        }
      }
    }
  }

  inherit( Node, RulerNode );

  /**
   * Creates a tick mark at a specific x location.
   * Each tick is marked at the top and bottom of the ruler.
   * If you desire a different style of tick mark, override this method.
   *
   * @param {number} x
   * @param {number} rulerHeight
   * @param {number} tickHeight
   * @param {String} stroke stroke color as a CSS string
   * @param {number} lineWidth
   * @return {Node}
   */
  var createTickMarkNode = function( x, rulerHeight, tickHeight, stroke, lineWidth ) {
    var shape = new Shape().moveTo( x, 0 ).lineTo( x, tickHeight ).moveTo( x, rulerHeight - tickHeight ).lineTo( x, rulerHeight );
    return new Path( shape, { stroke: stroke, lineWidth: lineWidth } );
  };

  return RulerNode;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Visual representation of the ruler.
 * This is a wrapper around the common-code ruler node.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/BLLRulerNode',['require','PHET_CORE/inherit','BEERS_LAW_LAB/common/view/MovableDragHandler','SCENERY/nodes/Node','SCENERY/nodes/Rectangle','SCENERY_PHET/RulerNode','KITE/Shape','BEERS_LAW_LAB/beers-law-lab-strings'], function( require ) {
  

  // imports
  var inherit = require( 'PHET_CORE/inherit' );
  var MovableDragHandler = require( 'BEERS_LAW_LAB/common/view/MovableDragHandler' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var RulerNode = require( 'SCENERY_PHET/RulerNode' );
  var Shape = require( 'KITE/Shape' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );

  /**
   * @param {Ruler} ruler
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function BLLRulerNode( ruler, mvt ) {

    var thisNode = this;
    Node.call( thisNode );

    // Compute tick labels, 1 major tick for every 0.5 unit of length, labels on the ticks that correspond to integer values.
    var majorTickLabels = [];
    var numberOfTicks = Math.floor( ruler.length / 0.5 ) + 1;
    for ( var i = 0; i < numberOfTicks; i++ ) {
      majorTickLabels[i] = ( i % 2 === 0 ) ? ( i / 2 ) : '';
    }

    // use the common ruler node
    var width = mvt.modelToViewDeltaX( ruler.length );
    var height = mvt.modelToViewDeltaY( ruler.height );
    var majorTickWidth = mvt.modelToViewDeltaX( 0.5 );
    thisNode.addChild( new RulerNode( width, height, majorTickWidth, majorTickLabels, strings.units_centimeters,
      { minorTicksPerMajorTick: 4, insetsWidth: 0 } ) );

    // touch area
    var dx = 0.05 * thisNode.width;
    var dy = 0.5 * thisNode.height;
    thisNode.touchArea = Shape.rectangle( -dx, -dy, thisNode.width + dx + dx, thisNode.height + dy + dy );

    // sync with model
    ruler.location.link( function( location ) {
      var position = mvt.modelToViewPosition( location );
      thisNode.x = position.x;
      thisNode.y = position.y;
    } );

    // interactivity
    thisNode.cursor = 'pointer';
    thisNode.addInputListener( new MovableDragHandler( ruler, mvt ) );
  }

  inherit( Node, BLLRulerNode );

  return BLLRulerNode;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Visual representation of the cuvette.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/CuvetteNode',['require','ASSERT/assert','DOT/Bounds2','SCENERY/util/Color','SCENERY_PHET/input/FillHighlightListener','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY/nodes/Rectangle','KITE/Shape','SCENERY/input/SimpleDragHandler','DOT/Util'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Color = require( 'SCENERY/util/Color' );
  var FillHighlightListener = require( 'SCENERY_PHET/input/FillHighlightListener' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Util = require( 'DOT/Util' );

  // constants
  var PERCENT_FULL = 0.92;
  var SOLUTION_ALPHA = 0.6;
  var ARROW_LENGTH = 110;
  var ARROW_HEAD_LENGTH = 38;
  var ARROW_HEAD_WIDTH = 45;
  var ARROW_TAIL_WIDTH = 23;
  var ARROW_FILL = Color.ORANGE;

  /**
   * Handler that is attached to the cuvette's drag handle.
   * @param {Node} cuvetteNode
   * @param {Cuvette} cuvette
   * @param {ModelViewTransform2} mvt
   * @param {Number} snapInterval
   * @constructor
   */
  function CuvetteDragHandler( cuvetteNode, cuvette, mvt, snapInterval ) {

    var startX; // x coordinate of mouse click
    var startWidth; // width of the cuvette when the drag started

    SimpleDragHandler.call( this, {

      allowTouchSnag: true,

      start: function( event ) {
        startX = event.pointer.point.x;
        startWidth = cuvette.width.get();
      },

      drag: function( event, trail ) {
        var dragX = event.pointer.point.x;
        var deltaWidth = mvt.viewToModelDeltaX( dragX - startX );
        var cuvetteWidth = Util.clamp( startWidth + deltaWidth, cuvette.widthRange.min, cuvette.widthRange.max );
        cuvette.width.set( cuvetteWidth );
      },

      end: function( event ) {
        var numberOfIntervals = Math.floor( ( cuvette.width.get() + ( snapInterval / 2 ) ) / snapInterval );
        cuvette.width.set( numberOfIntervals * snapInterval );
      },

      translate: function() {
        // override default behavior, do nothing
      }
    } );
  }

  inherit( SimpleDragHandler, CuvetteDragHandler );

  /**
   * @param {Cuvette} cuvette
   * @param {Property<BeersLawSolution>} solution
   * @param {ModelViewTransform2} mvt
   * @param {Number} snapInterval
   * @constructor
   */
  function CuvetteNode( cuvette, solution, mvt, snapInterval ) {

    var thisNode = this;
    Node.call( this );

    // A double-headed arrow, pointing left and right. Path definition starts at left tip and moves clockwise.
    assert && assert( ARROW_HEAD_WIDTH > ARROW_TAIL_WIDTH );
    assert && assert( ARROW_LENGTH > 2 * ARROW_HEAD_LENGTH );
    var arrowShape = new Shape()
      .moveTo( -ARROW_LENGTH / 2, 0 )
      .lineTo( -ARROW_LENGTH / 2 + ARROW_HEAD_LENGTH, -ARROW_HEAD_WIDTH / 2 )
      .lineTo( -ARROW_LENGTH / 2 + ARROW_HEAD_LENGTH, -ARROW_TAIL_WIDTH / 2 )
      .lineTo( ARROW_LENGTH / 2 - ARROW_HEAD_LENGTH, -ARROW_TAIL_WIDTH / 2 )
      .lineTo( ARROW_LENGTH / 2 - ARROW_HEAD_LENGTH, -ARROW_HEAD_WIDTH / 2 )
      .lineTo( ARROW_LENGTH / 2, 0 )
      .lineTo( ARROW_LENGTH / 2 - ARROW_HEAD_LENGTH, ARROW_HEAD_WIDTH / 2 )
      .lineTo( ARROW_LENGTH / 2 - ARROW_HEAD_LENGTH, ARROW_TAIL_WIDTH / 2 )
      .lineTo( -ARROW_LENGTH / 2 + ARROW_HEAD_LENGTH, ARROW_TAIL_WIDTH / 2 )
      .lineTo( -ARROW_LENGTH / 2 + ARROW_HEAD_LENGTH, ARROW_HEAD_WIDTH / 2 )
      .close();

    // nodes
    var cuvetteNode = new Path( null,{ stroke: 'black', lineWidth: 3 } );
    var solutionNode = new Rectangle( 0, 0, 1, 1, { lineWidth: 0.5 } );
    var arrowNode = new Path( arrowShape, {fill: ARROW_FILL, stroke: 'black', lineWidth: 1 } );

    // rendering order
    thisNode.addChild( solutionNode );
    thisNode.addChild( cuvetteNode );
    thisNode.addChild( arrowNode );

    // when the cuvette width changes ...
    cuvette.width.link( function() {
      var width = mvt.modelToViewDeltaX( cuvette.width.get() );
      var height = mvt.modelToViewDeltaY( cuvette.height );
      cuvetteNode.setShape( new Shape().moveTo( 0, 0 ).lineTo( 0, height ).lineTo( width, height ).lineTo( width, 0 ) );
      solutionNode.setRect( 0, 0, width, PERCENT_FULL * height );
      solutionNode.left = cuvetteNode.left;
      solutionNode.bottom = cuvetteNode.bottom;
      arrowNode.x = cuvetteNode.right;
      arrowNode.bottom = cuvetteNode.bottom - 10;
    } );

    // when the fluid color changes ...
    var colorObserver = function( color ) {
      solutionNode.fill = color.withAlpha( SOLUTION_ALPHA );
      solutionNode.stroke = color.darkerColor();
    };
    solution.get().fluidColor.link( colorObserver );

    // when the solution changes, rewire the color observer
    solution.link( function( newSolution, oldSolution ) {
      if ( oldSolution ) {
        oldSolution.fluidColor.unlink( colorObserver );
      }
      newSolution.fluidColor.link( colorObserver );
    } );

    // interactivity
    cuvetteNode.pickable = false;
    solutionNode.pickable = false;
    arrowNode.cursor = 'pointer';
    arrowNode.addInputListener( new FillHighlightListener( ARROW_FILL, ARROW_FILL.brighterColor() ) );
    arrowNode.addInputListener( new CuvetteDragHandler( thisNode, cuvette, mvt, snapInterval ) );

    // adjust touch area for the arrow
    var arrowBounds = arrowShape.computeBounds().copy();
    var dx = 0.25 * arrowBounds.width;
    var dy = 1 * arrowBounds.height;
    arrowNode.touchArea = Shape.rectangle( arrowBounds.minX - dx, arrowBounds.minY - dy, arrowBounds.width + ( 2 * dx ), arrowBounds.height + ( 2 * dy ) );

    // location of the cuvette
    var position = mvt.modelToViewPosition( cuvette.location );
    thisNode.x = position.x;
    thisNode.y = position.y;
  }

  inherit( Node, CuvetteNode );

  return CuvetteNode;
} );
define("image!BEERS_LAW_LAB/../images/light.png", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/png;base64,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';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Visual representation of the light in the Beer's Law screen.
 * Origin is at the right center, where the light comes out of the 'housing'.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/LightNode',['require','SCENERY/nodes/Image','PHET_CORE/inherit','SCENERY/nodes/Node','AXON/Property','KITE/Shape','BEERS_LAW_LAB/common/view/ToggleButton','image!BEERS_LAW_LAB/../images/light.png','image!BEERS_LAW_LAB/../images/red_button_pressed.png','image!BEERS_LAW_LAB/../images/red_button_unpressed.png','image!BEERS_LAW_LAB/../images/red_button_disabled.png'], function( require ) {
  

  // imports
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Property = require( 'AXON/Property' );
  var Shape = require( 'KITE/Shape' );
  var ToggleButton = require( 'BEERS_LAW_LAB/common/view/ToggleButton' );

  // images
  var lightImage = require( 'image!BEERS_LAW_LAB/../images/light.png' );
  var buttonPressedImage = require( 'image!BEERS_LAW_LAB/../images/red_button_pressed.png' );
  var buttonUnpressedImage = require( 'image!BEERS_LAW_LAB/../images/red_button_unpressed.png' );
  var buttonDisabledImage = require( 'image!BEERS_LAW_LAB/../images/red_button_disabled.png' );

  /**
   * @param {Light} light
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function LightNode( light, mvt ) {

    var thisNode = this;
    Node.call( this );

    // nodes
    var housing = new Image( lightImage, { pickable: false } );
    var button = new ToggleButton( buttonUnpressedImage, buttonPressedImage, buttonDisabledImage, light.on, new Property( true ) );

    // expand touch area for button. Do this before scaling the button!
    button.touchArea = Shape.circle( button.width/2, button.height/2, 1.0 * button.width /* radius */ );

    // make the button fit in the housing
    button.setScaleMagnitude( 0.65 * housing.height / button.height );

    // rendering order
    thisNode.addChild( housing );
    thisNode.addChild( button );

    // layout
    housing.x = -housing.width;
    housing.y = -housing.height / 2;
    button.left = housing.right - button.width - 40;
    button.centerY = housing.centerY;

    // position
    var position = mvt.modelToViewPosition( light.location );
    thisNode.x = position.x;
    thisNode.y = position.y;
  }

  inherit( Node, LightNode );

  return LightNode;
} );
// Copyright 2002-2013, University of Colorado Boulder

//TODO generalize this so that any node can be put on the button
//TODO Handle press-and-hold feature in Sim.animationLoop instead of using timers.
//TODO This implementation should eventually use sun.Button
/**
 * Button with an arrow that points up, down, left or right.
 * Press and release immediately and the button fires on 'up'.
 * Press and hold for M milliseconds and the button will fire repeatedly every N milliseconds until released.
 *
 * @author Chris Malley (PixelZoom, Inc)
 */
define('SCENERY_PHET/ArrowButton',['require','SCENERY/input/ButtonListener','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY/nodes/Rectangle','KITE/Shape'], function( require ) {
  

  // imports
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );

  /**
   * @param {String} direction 'up'|'down'|'left'|'right'
   * @param callback
   * @param options
   * @constructor
   */
  function ArrowButton( direction, callback, options ) {

    var thisButton = this;

    var DEFAULT_ARROW_WIDTH = 20;
    options = _.extend( {
        arrowHeight: DEFAULT_ARROW_WIDTH,
        arrowWidth: DEFAULT_ARROW_WIDTH * Math.sqrt( 3 ) / 2,
        fill: 'white',
        stroke: 'black',
        lineWidth: 1,
        xMargin: 7,
        yMargin: 5,
        cornerRadius: 4,
        enabledFill: 'black',
        disabledFill: 'rgb(175,175,175)',
        enabledStroke: 'black',
        disabledStroke: 'rgb(175,175,175)',
        timerDelay: 400, // start to fire continuously after pressing for this long (milliseconds)
        intervalDelay: 100 // fire continuously at this frequency (milliseconds)
      },
      options );

    Node.call( thisButton );

    // nodes
    var arrowShape;
    if ( direction === 'up' ) {
      arrowShape = new Shape().moveTo( options.arrowWidth / 2, 0 ).lineTo( options.arrowWidth, options.arrowHeight ).lineTo( 0, options.arrowHeight ).close();
    }
    else if ( direction === 'down' ) {
      arrowShape = new Shape().moveTo( 0, 0 ).lineTo( options.arrowWidth, 0 ).lineTo( options.arrowWidth / 2, options.arrowHeight ).close();
    }
    else if ( direction === 'left' ) {
      arrowShape = new Shape().moveTo( 0, options.arrowHeight / 2 ).lineTo( options.arrowWidth, 0 ).lineTo( options.arrowWidth, options.arrowHeight ).close();
    }
    else if ( direction === 'right' ) {
      arrowShape = new Shape().moveTo( 0, 0 ).lineTo( options.arrowWidth, options.arrowHeight / 2 ).lineTo( 0, options.arrowHeight ).close();
    }
    else {
      throw new Error( "unsupported direction: " + direction );
    }
    var arrowNode = new Path( arrowShape, { fill: options.enabledFill, pickable: false } );
    var background = new Rectangle( 0, 0, arrowNode.width + ( 2 * options.xMargin ), arrowNode.height + ( 2 * options.yMargin ), options.cornerRadius, options.cornerRadius,
      {stroke: options.stroke, lineWidth: options.lineWidth, fill: options.fill, pickable: false } );

    // rendering order
    thisButton.addChild( background );
    thisButton.addChild( arrowNode );

    // layout
    arrowNode.centerX = background.centerX;
    arrowNode.centerY = background.centerY;

    // touch area
    var dx = 0.25 * thisButton.width;
    var dy = 0.25 * thisButton.height;
    thisButton.touchArea = Shape.rectangle( -dx, -dy, thisButton.width + dx + dx, thisButton.height + dy + dy );

    // mouse area is constrained to the tight rectangle, so that we can make the children unpickable
    thisButton.mouseArea = Shape.rectangle( 0, 0, thisButton.width, thisButton.height );

    // interactivity
    thisButton.cursor = 'pointer';
    var enabled = true;
    var fired = false;
    var timeoutID = null;
    var intervalID = null;
    var cleanupTimer = function() {
      if ( timeoutID ) {
        window.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID ) {
        window.clearInterval( intervalID );
        intervalID = null;
      }
    };
    thisButton.addInputListener( new ButtonListener( {

      over: function() {
        //TODO highlight
      },

      down: function() {
        fired = false;
        timeoutID = window.setTimeout( function() {
          timeoutID = null;
          fired = true;
          intervalID = window.setInterval( function() {
            if ( enabled ) {
              callback();
            }
          }, options.intervalDelay );
        }, options.timerDelay );
      },

      up: function() {
        cleanupTimer();
      },

      fire: function() {
        cleanupTimer();
        if ( !fired && enabled ) {
          callback();
        }
      }
    } ) );

    //TODO consider adding this to the prototype. Would need to convert several local vars to fields, and cleanupTimer gets a little complicated.
    thisButton.setEnabled = function( value ) {
      enabled = value;
      if ( !enabled ) {
        cleanupTimer();
      }
      arrowNode.fill = enabled ? options.enabledFill : options.disabledFill;
      background.stroke = enabled ? options.enabledStroke : options.disabledStroke;
      thisButton.pickable = enabled;
    };
    thisButton.setEnabled( true );

    thisButton.mutate( options );
  }

  inherit( Node, ArrowButton );

  return ArrowButton;
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Slider for changing a solution's concentration.
 * <p>
 * Note that this slider has some things in common with HSlider, but also subtle differences.
 * So it's probably more trouble than it's worth to factor out a base class.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/ConcentrationSlider',['require','SCENERY_PHET/ArrowButton','SCENERY/util/Color','DOT/Dimension2','SCENERY_PHET/input/FillHighlightListener','PHET_CORE/inherit','DOT/LinearFunction','SCENERY/util/LinearGradient','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY_PHET/PhetFont','SCENERY/nodes/Rectangle','KITE/Shape','SCENERY/input/SimpleDragHandler','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var ArrowButton = require( 'SCENERY_PHET/ArrowButton' );
  var Color = require( 'SCENERY/util/Color' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var FillHighlightListener = require( 'SCENERY_PHET/input/FillHighlightListener' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Text = require( 'SCENERY/nodes/Text' );

  // track constants
  var TRACK_SIZE = new Dimension2( 200, 15 );

  // thumb constants
  var THUMB_SIZE = new Dimension2( 22, 45 );
  var THUMB_LINE_WIDTH = 1;
  var THUMB_FILL_NORMAL = new Color( 89, 156, 212 );
  var THUMB_FILL_HIGHLIGHT = THUMB_FILL_NORMAL.brighterColor();
  var THUMB_STROKE = Color.BLACK;
  var THUMB_CENTER_LINE_STROKE = Color.WHITE;

  // tick constants
  var TICK_LENGTH = 16;
  var TICK_FONT = new PhetFont( 16 );
  var TICK_DECIMAL_PLACES = 0;

  /**
   * Track that the thumb moves in.
   * Filled with a gradient that matches the solution.
   * Clicking in the track changes the value.
   *
   * @param {Dimension2} trackSize
   * @param {Property<BeersLawSolution>} solution
   * @constructor
   */
  function Track( trackSize, solution ) {

    var thisNode = this;
    Rectangle.call( thisNode, 0, 0, trackSize.width, trackSize.height,
      { cursor: 'pointer', stroke: 'black', lineWidth: 1 } );

    // sync view with model
    var positionToConcentration;
    solution.link( function( solution ) {
      // change the view-to-model function to match the solution's concentration range
      var concentrationRange = solution.concentrationRange;
      positionToConcentration = new LinearFunction( 0, trackSize.width, concentrationRange.min, concentrationRange.max, true /* clamp */ );

      // fill with a gradient that matches the solution's color range
      thisNode.fill = new LinearGradient( 0, 0, trackSize.width, 0 )
        .addColorStop( 0, solution.colorRange.min )
        .addColorStop( 1, solution.colorRange.max );
    } );

    // click in the track to change the value, continue dragging if desired
    var handleEvent = function( event ) {
      var x = thisNode.globalToLocalPoint( event.pointer.point ).x;
      var concentration = positionToConcentration( x );
      solution.get().concentration.set( concentration );
    };
    thisNode.addInputListener( new SimpleDragHandler(
      {
        start: function( event ) {
          handleEvent( event );
        },
        drag: function( event ) {
          handleEvent( event );
        },
        translate: function() {
          // do nothing, override default behavior
        }
      } ) );
  }

  inherit( Rectangle, Track );

  /**
   * Vertical tick line.
   * @constructor
   */
  function TickLine() {
    Path.call( this, Shape.lineSegment( 0, 0, 0, TICK_LENGTH ), { stroke: 'black', lineWidth: 1 } );
  }

  inherit( Path, TickLine );

  /**
   * Tick label.
   * @param {Number} value
   * @constructor
   */
  function TickLabel( value ) {
    var thisNode = this;
    Text.call( thisNode, '?', { font: TICK_FONT, fill: 'black' } );
    thisNode.setValue = function( value ) {
      thisNode.text = value.toFixed( TICK_DECIMAL_PLACES );
    };
    thisNode.setValue( value );
  }

  inherit( Text, TickLabel );

  /**
   * The slider thumb, a rounded rectangle with a vertical line through its center.
   * @param {Dimension2} thumbSize
   * @param {Dimension2} trackSize
   * @param {Property<BeersLawSolution>} solution
   * @constructor
   */
  function Thumb( thumbSize, trackSize, solution ) {

    var thisNode = this;
    Node.call( thisNode, { cursor: 'pointer' } );

    // nodes
    var arcWidth = 0.25 * thumbSize.width;
    var body = new Rectangle( -thumbSize.width / 2, -thumbSize.height / 2, thumbSize.width, thumbSize.height, arcWidth, arcWidth,
      { fill: THUMB_FILL_NORMAL, stroke: THUMB_STROKE, lineWidth: THUMB_LINE_WIDTH } );
    var centerLineYMargin = 3;
    var centerLine = new Path( Shape.lineSegment( 0, -( thumbSize.height / 2 ) + centerLineYMargin, 0, ( thumbSize.height / 2 ) - centerLineYMargin ), { stroke: THUMB_CENTER_LINE_STROKE } );

    // rendering order
    thisNode.addChild( body );
    thisNode.addChild( centerLine );

    // interactivity
    body.addInputListener( new FillHighlightListener( THUMB_FILL_NORMAL, THUMB_FILL_HIGHLIGHT ) );

    // touch area
    var dx = 0.25 * thisNode.width;
    var dy = 0.5 * thisNode.height;
    thisNode.touchArea = Shape.rectangle( ( -thisNode.width / 2 ) - dx, ( -thisNode.height / 2 ) - dy, thisNode.width + dx + dx, thisNode.height + dy + dy );

    // set the drag handler and mapping function for the selected solution
    var dragHandler, concentrationToPosition;
    var setSolution = function( solution ) {
      // drag handler with solution's concentration range
      if ( dragHandler ) {
        thisNode.removeInputListener( dragHandler );
      }
      dragHandler = new ThumbDragHandler( thisNode, solution.concentration, new LinearFunction( 0, trackSize.width, solution.concentrationRange.min, solution.concentrationRange.max, true /* clamp */ ) );
      thisNode.addInputListener( dragHandler );

      // linear mapping function with solution's concentration range
      concentrationToPosition = new LinearFunction( solution.concentrationRange.min, solution.concentrationRange.max, 0, trackSize.width, true /* clamp */ );
    };
    setSolution( solution.get() );

    // move the slider thumb to reflect the concentration value
    var concentrationObserver = function( concentration ) {
      thisNode.x = concentrationToPosition( concentration );
    };
    solution.get().concentration.link( concentrationObserver );

    // when the solution changes, wire up to the current solution
    solution.link( function( newSolution, oldSolution ) {
      setSolution( newSolution );
      if ( oldSolution ) {
        oldSolution.concentration.unlink( concentrationObserver );
      }
      newSolution.concentration.link( concentrationObserver );
    } );
  }

  inherit( Node, Thumb );

  /**
   * Drag handler for the slider thumb.
   * @param {Node} dragNode
   * @param {Property<Number>} concentration
   * @param {LinearFunction} positionToValue
   * @constructor
   */
  function ThumbDragHandler( dragNode, concentration, positionToValue ) {
    var clickXOffset; // x-offset between initial click and thumb's origin
    SimpleDragHandler.call( this, {
      allowTouchSnag: true,
      start: function( event ) {
        clickXOffset = dragNode.globalToParentPoint( event.pointer.point ).x - event.currentTarget.x;
      },
      drag: function( event ) {
        var x = dragNode.globalToParentPoint( event.pointer.point ).x - clickXOffset;
        concentration.set( positionToValue( x ) );
      },
      translate: function() {
        // do nothing, override default behavior
      }
    } );
  }

  inherit( SimpleDragHandler, ThumbDragHandler );

  /**
   * @param {Property<BeersLawSolution>} solution
   * @constructor
   */
  function ConcentrationSlider( solution ) {

    var thisNode = this;
    Node.call( thisNode );

    // nodes
    var track = new Track( TRACK_SIZE, solution );
    var thumb = new Thumb( THUMB_SIZE, TRACK_SIZE, solution );
    var minTickLine = new TickLine();
    var maxTickLine = new TickLine();
    var minTickLabel = new TickLabel( 0 ); // correct value will be set when observer is registered
    var maxTickLabel = new TickLabel( 0 ); // correct value will be set when observer is registered

    // buttons for single-unit increments
    var plusButton = new ArrowButton( 'right', function() {
      solution.get().concentration.set( Math.min( solution.get().concentration.get() + solution.get().concentrationTransform.viewToModel( 1 ), solution.get().concentrationRange.max ) );
    } );
    var minusButton = new ArrowButton( 'left', function() {
      solution.get().concentration.set( Math.max( solution.get().concentration.get() - solution.get().concentrationTransform.viewToModel( 1 ), solution.get().concentrationRange.min ) );
    } );

    // rendering order
    thisNode.addChild( minTickLine );
    thisNode.addChild( maxTickLine );
    thisNode.addChild( minTickLabel );
    thisNode.addChild( maxTickLabel );
    thisNode.addChild( track );
    thisNode.addChild( thumb );
    thisNode.addChild( plusButton );
    thisNode.addChild( minusButton );

    // layout
    minTickLine.left = track.left;
    minTickLine.bottom = track.top;
    minTickLabel.bottom = minTickLine.top - 2;
    maxTickLine.right = track.right;
    maxTickLine.bottom = track.top;
    maxTickLabel.bottom = maxTickLine.top - 2;
    thumb.centerY = track.centerY;
    minusButton.right = track.left - ( thumb.width / 2 ) - 2;
    minusButton.bottom = track.bottom;
    plusButton.left = track.right + ( thumb.width / 2 ) + 2;
    plusButton.bottom = track.bottom;

    var concentrationObserver = function( concentration ) {
      // buttons
      plusButton.setEnabled( concentration < solution.get().concentrationRange.max );
      minusButton.setEnabled( concentration > solution.get().concentrationRange.min );
    };

    // update the tick labels to match the solution
    solution.link( function( solution, oldSolution ) {
      var concentrationRange = solution.concentrationRange;
      var transform = solution.concentrationTransform;
      // update label values
      minTickLabel.setValue( transform.modelToView( concentrationRange.min ) );
      maxTickLabel.setValue( transform.modelToView( concentrationRange.max ) );
      // center values below tick lines
      minTickLabel.centerX = minTickLine.centerX;
      maxTickLabel.centerX = maxTickLine.centerX;
      // re-wire observer
      if ( oldSolution ) {
        oldSolution.concentration.unlink( concentrationObserver );
      }
      solution.concentration.link( concentrationObserver );
    } );
  }

  inherit( Node, ConcentrationSlider );

  return ConcentrationSlider;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Control for changing solution's concentration.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/ConcentrationControl',['require','BEERS_LAW_LAB/beerslaw/view/ConcentrationSlider','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY_PHET/PhetFont','SCENERY/nodes/Rectangle','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var ConcentrationSlider = require( 'BEERS_LAW_LAB/beerslaw/view/ConcentrationSlider' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );

  // constants
  var FONT = new PhetFont( 20 );

  /**
   * @param {Property<BeersLawSolution>} solution
   * @constructor
   */
  function ConcentrationControl( solution ) {

    var thisNode = this;
    Node.call( thisNode );

    // nodes
    var label = new Text( StringUtils.format( strings.pattern_0label, strings.concentration ), { font: FONT } );
    var slider = new ConcentrationSlider( solution );
    var valueDisplay = new Text( ' ' + solution.get().getDisplayConcentration( solution.get().concentrationRange.max ), { font: FONT } );
    var xMargin = 0.1 * valueDisplay.width;
    var yMargin = 0.1 * valueDisplay.height;
    var valueBackground = new Rectangle( 0, 0, valueDisplay.width + xMargin + xMargin, valueDisplay.height + yMargin + yMargin,
      { fill: 'white', stroke: 'lightGray' } );

    // rendering order
    thisNode.addChild( label );
    thisNode.addChild( slider );
    thisNode.addChild( valueBackground );
    thisNode.addChild( valueDisplay );

    // layout
    valueBackground.left = label.right + 6;
    valueDisplay.right = valueBackground.right - xMargin; // right aligned
    valueDisplay.y = label.y; // align baselines
    valueBackground.centerY = valueDisplay.centerY;
    slider.left = valueDisplay.right + 20;
    slider.centerY = valueDisplay.centerY;

    // update the value display when concentration changes
    var concentrationObserver = function( concentration ) {
      valueDisplay.text = solution.get().getDisplayConcentration( concentration );
      valueDisplay.right = valueBackground.right - xMargin; // right aligned
    };
    solution.get().concentration.link( concentrationObserver );

    // when solution changes, rewire the concentration observer
    solution.link( function( newSolution, oldSolution ) {
      if ( oldSolution ) {
        oldSolution.concentration.unlink( concentrationObserver );
      }
      newSolution.concentration.link( concentrationObserver );
    } );
  }

  inherit( Node, ConcentrationControl );

  return ConcentrationControl;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Combo box for selecting solutions.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/SolutionComboBox',['require','ASSERT/assert','SUN/ComboBox','SCENERY/nodes/HTMLText','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY_PHET/PhetFont','SCENERY/nodes/Rectangle','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text'], function( require ) {
  

  // imports
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var ComboBox = require( 'SUN/ComboBox' );
  var HTMLText = require( 'SCENERY/nodes/HTMLText' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );

  /**
   * @param {Array<BeersLawSolution>} solutions
   * @param {Property<BeersLawSolution>} selectedSolution
   * @param {Node} soluteListParent
   * @constructor
   */
  function SolutionComboBox( solutions, selectedSolution, soluteListParent ) {

    // 'Solution' label
    var label = new Text( StringUtils.format( strings.pattern_0label, strings.solution ), { font: new PhetFont( 20 ) } );

    // items
    var items = [];
    for ( var i = 0; i < solutions.length; i++ ) {
      var solution = solutions[i];
      items[i] = createItem( solution );
    }

    ComboBox.call( this, items, selectedSolution, soluteListParent, {
      labelNode: label,
      listPosition: 'above',
      itemYMargin: 12,
      itemHighlightFill: 'rgb(218,255,255)'
    } );
  }

  inherit( ComboBox, SolutionComboBox );

  /**
   * Creates a combo box item.
   * @param {Solution} solution
   * @returns {{node: *, value: *}}
   * @private
   */
  var createItem = function( solution ) {

    // node
    var node = new Node();
    var colorSquare = new Rectangle( 0, 0, 20, 20,
      { fill: solution.saturatedColor, stroke: solution.saturatedColor.darkerColor() } );
    var solutionName = new HTMLText( solution.getDisplayName(), { font: new PhetFont( 20 ) } );
    node.addChild( colorSquare );
    node.addChild( solutionName );
    solutionName.left = colorSquare.right + 5;
    solutionName.centerY = colorSquare.centerY;

    return ComboBox.createItem( node, solution );
  };

  return SolutionComboBox;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Control panel for solution.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/SolutionControls',['require','BEERS_LAW_LAB/beerslaw/view/ConcentrationControl','PHET_CORE/inherit','SCENERY/nodes/Node','SUN/Panel','BEERS_LAW_LAB/beerslaw/view/SolutionComboBox'], function( require ) {
  

  // imports
  var ConcentrationControl = require( 'BEERS_LAW_LAB/beerslaw/view/ConcentrationControl' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Panel = require( 'SUN/Panel' );
  var SolutionComboBox = require( 'BEERS_LAW_LAB/beerslaw/view/SolutionComboBox' );

  /**
   * @param {Array<BeersLawSolution>} solutions
   * @param {Property<BeersLawSolution>} currentSolution
   * @param {Node} solutionListParent
   * @constructor
   */
  function SolutionControls( solutions, currentSolution, solutionListParent ) {

    // nodes
    var comboBox = new SolutionComboBox( solutions, currentSolution, solutionListParent );
    var concentrationControl = new ConcentrationControl( currentSolution );
    var contentNode = new Node();

    // rendering order
    contentNode.addChild( concentrationControl );
    contentNode.addChild( comboBox );

    // layout
    concentrationControl.left = comboBox.left;
    concentrationControl.top = comboBox.bottom + 15;

    Panel.call( this, contentNode,
      { xMargin: 15, yMargin: 15, fill: '#F0F0F0', stroke: 'gray', lineWidth: 1, resize: false } );
  }

  inherit( Panel, SolutionControls );

  return SolutionControls;
} );
// Copyright 2002-2013, University of Colorado Boulder

define('SCENERY_PHET/../nls/scenery-phet-strings', {
  "root": true
} );
// Copyright 2002-2013, University of Colorado Boulder

define('SCENERY_PHET/../nls/root/scenery-phet-strings', {
  "pattern_0value_1units": "{0} {1}",
  "units_nm": "nm"
} );

// Copyright 2002-2013, University of Colorado Boulder

/**
 * Load the strings only once and from a single location, because loading them from different relative paths
 * causes them to fall back to the root language sometimes for unknown reasons.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/scenery-phet-strings',['require','i18n!SCENERY_PHET/../nls/scenery-phet-strings'], function( require ) {
  
  return require( 'i18n!SCENERY_PHET/../nls/scenery-phet-strings' );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * WavelengthSlider is a slider-like control used for setting visible wavelength.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('SCENERY_PHET/WavelengthSlider',['require','SCENERY_PHET/ArrowButton','ASSERT/assert','DOT/Dimension2','SCENERY/nodes/Image','PHET_CORE/inherit','SCENERY/nodes/Node','SCENERY/nodes/Path','SCENERY_PHET/PhetFont','SCENERY/nodes/Rectangle','KITE/Shape','SCENERY/input/SimpleDragHandler','SCENERY_PHET/scenery-phet-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text','DOT/Util','SCENERY_PHET/VisibleColor'], function( require ) {
  

  // imports
  var ArrowButton = require( 'SCENERY_PHET/ArrowButton' );
  var assert = require( 'ASSERT/assert' )( 'beers-law-lab' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var strings = require( 'SCENERY_PHET/scenery-phet-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var VisibleColor = require( 'SCENERY_PHET/VisibleColor' );

  /**
   * Slider track that displays the visible spectrum.
   * @param width
   * @param height
   * @param minWavelength
   * @param maxWavelength
   * @constructor
   */
  function Track( width, height, minWavelength, maxWavelength ) {

    Node.call( this );

    // Draw the spectrum directly to a canvas, to improve performance.
    var canvas = document.createElement( 'canvas' );
    var context = canvas.getContext( '2d' );
    canvas.width = width;
    canvas.height = height;
    for ( var i = 0; i < width; i++ ) {
      var wavelength = Util.clamp( Util.linear( 0, width, minWavelength, maxWavelength, i ), minWavelength, maxWavelength );  // position -> wavelength
      context.fillStyle = VisibleColor.wavelengthToColor( wavelength ).toCSS();
      context.fillRect( i, 0, 1, 50 );
    }

    this.addChild( new Image( canvas ) );
  }

  inherit( Node, Track );

  /**
   * The slider thumb (aka knob)
   * @param {Number} width
   * @param {Number} height
   * @constructor
   */
  function Thumb( width, height ) {
    var shape = new Shape()
      .moveTo( 0, 0 )
      .lineTo( 0.5 * width, 0.3 * height )
      .lineTo( 0.5 * width, 1 * height )
      .lineTo( -0.5 * width, 1 * height )
      .lineTo( -0.5 * width, 0.3 * height )
      .close();
    Path.call( this, shape, { stroke: 'black', lineWidth: 1, fill: 'black' } );

    // touch area, don't extend above so that we don't encroach on slider track
    var bounds = shape.computeBounds().copy();
    var dx = 0.5 * bounds.width;
    var dy = 0.5 * bounds.height;
    this.touchArea = Shape.rectangle( bounds.minX - dx, bounds.minY, bounds.width + dx + dx, bounds.height + dy + dy );
  }

  inherit( Path, Thumb );

  /**
   * Displays the value and units.
   * @param property
   * @param {String} font
   * @param {String} fill
   * @constructor
   */
  function ValueDisplay( property, font, fill ) {
    var thisNode = this;
    Text.call( this, '?', { font: font, fill: fill } );
    property.link( function( value ) {
      thisNode.text = StringUtils.format( strings.pattern_0value_1units, value.toFixed( 0 ), strings.units_nm );
    } );
  }

  inherit( Text, ValueDisplay );

  /**
   * Rectangular 'cursor' that appears in the track directly above the thumb. Origin is at top center of cursor.
   * @param {Number} width
   * @param {Number} height
   * @constructor
   */
  function Cursor( width, height ) {
    Rectangle.call( this, -width / 2, 0, width, height, { stroke: 'black', lineWidth: 1 } );
  }

  inherit( Rectangle, Cursor );

  /**
   * @param {Property<Number>} wavelength
   * @param {object} options
   * @constructor
   */
  function WavelengthSlider( wavelength, options ) {

    // options that are specific to this type
    options = _.extend( {
      minWavelength: VisibleColor.MIN_WAVELENGTH,
      maxWavelength: VisibleColor.MAX_WAVELENGTH,
      trackWidth: 150,
      trackHeight: 30,
      thumbWidth: 35,
      thumbHeight: 45,
      valueFont: new PhetFont( 20 ),
      valueFill: 'black',
      valueVisible: false
    }, options );

    // validate wavelengths
    assert && assert( options.minWavelength < options.maxWavelength );
    assert && assert( options.minWavelength >= VisibleColor.MIN_WAVELENGTH && options.minWavelength <= VisibleColor.MAX_WAVELENGTH );
    assert && assert( options.maxWavelength >= VisibleColor.MIN_WAVELENGTH && options.maxWavelength <= VisibleColor.MAX_WAVELENGTH );

    var thisNode = this;
    Node.call( thisNode, options );

    var thumb = new Thumb( options.thumbWidth, options.thumbHeight );
    var valueDisplay = new ValueDisplay( wavelength, options.valueFont, options.valueFill );
    var track = new Track( options.trackWidth, options.trackHeight, options.minWavelength, options.maxWavelength );
    var cursor = new Cursor( 3, track.height );

    // buttons for single-unit increments
    var plusButton = new ArrowButton( 'right', function() {
      wavelength.set( wavelength.get() + 1 );
    } );
    var minusButton = new ArrowButton( 'left', function() {
      wavelength.set( wavelength.get() - 1 );
    } );

    /*
     * Put a border around the track.
     * We don't stroke the track itself because stroking the track will affect its bounds,
     * and will thus affect the drag handle behavior.
     * Having a separate border also gives subclasses a place to add markings (eg, tick marks)
     * without affecting the track's bounds.
     */
    var trackBorder = new Rectangle( 0, 0, track.width, track.height, { stroke: 'black', lineWidth: 1, pickable: false } );

    // rendering order
    thisNode.addChild( track );
    thisNode.addChild( trackBorder );
    thisNode.addChild( thumb );
    if ( options.valueVisible ) {
      thisNode.addChild( valueDisplay );
    }
    thisNode.addChild( cursor );
    thisNode.addChild( plusButton );
    thisNode.addChild( minusButton );

    // layout
    cursor.top = track.top;
    thumb.top = track.bottom;
    valueDisplay.bottom = track.top - 2;
    plusButton.left = track.right + 8;
    plusButton.centerY = track.centerY;
    minusButton.right = track.left - 8;
    minusButton.centerY = track.centerY;

    // transforms between position and wavelength
    var positionToWavelength = function( x ) {
      return Math.floor( Util.clamp( Util.linear( 0, track.width, options.minWavelength, options.maxWavelength, x ), options.minWavelength, options.maxWavelength ) );
    };
    var wavelengthToPosition = function( wavelength ) {
      return Math.floor( Util.clamp( Util.linear( options.minWavelength, options.maxWavelength, 0, track.width, wavelength ), 0, track.width ) );
    };

    // track interactivity
    track.cursor = 'pointer';
    track.addInputListener(
      {
        down: function( event ) {
          var x = track.globalToParentPoint( event.pointer.point ).x;
          wavelength.set( positionToWavelength( x ) );
        }
      } );

    // thumb interactivity
    thumb.cursor = 'pointer';
    var clickXOffset = 0; // x-offset between initial click and thumb's origin
    thumb.addInputListener( new SimpleDragHandler(
      {
        allowTouchSnag: true,

        start: function( event ) {
          clickXOffset = thumb.globalToParentPoint( event.pointer.point ).x - thumb.x;
        },

        drag: function( event ) {
          var x = thumb.globalToParentPoint( event.pointer.point ).x - clickXOffset;
          wavelength.set( positionToWavelength( x ) );
        },

        translate: function() {
          // do nothing, override default behavior
        }
      } ) );

    // sync with model
    var updateUI = function( wavelength ) {
      // positions
      var x = wavelengthToPosition( wavelength );
      thumb.centerX = x;
      cursor.centerX = x;
      valueDisplay.centerX = x;
      // thumb color
      thumb.fill = VisibleColor.wavelengthToColor( wavelength );
      // plus and minus buttons
      plusButton.setEnabled( wavelength < options.maxWavelength );
      minusButton.setEnabled( wavelength > options.minWavelength );
    };
    wavelength.link( function( wavelength ) {
      updateUI( wavelength );
    } );

    /*
     * The horizontal bounds of the wavelength control changes as the slider knob is dragged.
     * To prevent this, we determine the extents of the control's bounds at min and max values,
     * then add an invisible horizontal strut.
     */
    {
      // determine bounds at min and max wavelength settings
      updateUI( options.minWavelength );
      var minX = thisNode.left;
      updateUI( options.maxWavelength );
      var maxX = thisNode.right;

      // restore the wavelength
      updateUI( wavelength.get() );

      // add a horizontal strut
      var strut = new Rectangle( minX, 0, maxX - minX, 1, { pickable: false } );
      thisNode.addChild( strut );
      strut.moveToBack();
    }
  }

  return inherit( Node, WavelengthSlider );
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Control for wavelength.
 *
 * @author Chris Malley (PixelZoom, Inc)
 */
define('BEERS_LAW_LAB/beerslaw/view/WavelengthControls',['require','DOT/Dimension2','PHET_CORE/inherit','SCENERY/nodes/Node','SUN/Panel','SCENERY_PHET/PhetFont','AXON/Property','SUN/AquaRadioButton','SCENERY/nodes/Rectangle','BEERS_LAW_LAB/beers-law-lab-strings','PHETCOMMON/util/StringUtils','SCENERY/nodes/Text','SCENERY_PHET/WavelengthSlider'], function( require ) {
  

  // imports
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Panel = require( 'SUN/Panel' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var Property = require( 'AXON/Property' );
  var AquaRadioButton = require( 'SUN/AquaRadioButton' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );
  var WavelengthSlider = require( 'SCENERY_PHET/WavelengthSlider' );

  /**
   * @param {Property<BeersLawSolution>} solution
   * @param {Light} light
   * @constructor
   */
  function WavelengthControls( solution, light ) {

    var thisNode = this;

    var variableWavelength = new Property( false ); // is the wavelength variable or fixed?

    // nodes
    var label = new Text( StringUtils.format( strings.pattern_0label, strings.wavelength ), { font: new PhetFont( 20 ), fill: 'black' } );
    var valueDisplay = new Text( thisNode.formatWavelength( light.wavelength.get() ), { font: new PhetFont( 20 ), fill: 'black' } );
    var xMargin = 0.1 * valueDisplay.width;
    var yMargin = 0.1 * valueDisplay.height;
    var valueBackground = new Rectangle( 0, 0, valueDisplay.width + xMargin + xMargin, valueDisplay.height + yMargin + yMargin,
      { fill: 'white', stroke: 'lightGray' } );
    var presetRadioButton = new AquaRadioButton( variableWavelength, false, new Text( strings.preset, { font: new PhetFont( 18 ), fill: 'black' } ) );
    var variableRadioButton = new AquaRadioButton( variableWavelength, true, new Text( strings.variable, { font: new PhetFont( 18 ), fill: 'black' } ) );
    var wavelengthSlider = new WavelengthSlider( light.wavelength, { trackWidth: 150, trackHeight: 30 } );

    // rendering order
    var content = new Node();
    content.addChild( label );
    content.addChild( valueBackground );
    content.addChild( valueDisplay );
    content.addChild( presetRadioButton );
    if ( !variableWavelength.get() ) { // opposite of initial state, so variableWavelength.link doesn't fail on add/removeChild
      content.addChild( variableRadioButton );
    }
    content.addChild( wavelengthSlider );

    // add a horizontal strut to prevent width changes
    content.addChild( new Rectangle( 0, 0, content.width, 1, { pickable: false } ) );

    // layout
    var ySpacing = 20;
    valueBackground.left = label.right + 10;
    valueDisplay.right = valueBackground.right - xMargin; // right aligned
    valueDisplay.y = label.y; // align baselines
    valueBackground.centerY = valueDisplay.centerY;
    presetRadioButton.left = label.left;
    presetRadioButton.top = label.bottom + ySpacing;
    variableRadioButton.left = presetRadioButton.right + 15;
    variableRadioButton.centerY = presetRadioButton.centerY;
    wavelengthSlider.left = presetRadioButton.left;
    wavelengthSlider.top = presetRadioButton.bottom + ySpacing;

    Panel.call( thisNode, content,
      { xMargin: 20, yMargin: 20, fill: '#F0F0F0', stroke: 'gray', lineWidth: 1 } );

    // When the radio button selection changes...
    variableWavelength.link( function( isVariable ) {
      if ( isVariable ) {
        content.addChild( wavelengthSlider );
      }
      else {
        content.removeChild( wavelengthSlider );
      }
      if ( !isVariable ) {
        // Set the light to the current solution's lambdaMax wavelength.
        light.wavelength.set( solution.get().molarAbsorptivityData.lambdaMax );
      }
    } );

    thisNode.reset = function() {
      variableWavelength.reset();
    };

    // sync displayed value with model
    light.wavelength.link( function( wavelength ) {
      valueDisplay.text = thisNode.formatWavelength( wavelength );
      valueDisplay.right = valueBackground.right - xMargin; // right aligned
    } );
  }

  inherit( Panel, WavelengthControls, {
    formatWavelength: function( wavelength ) {
      return StringUtils.format( strings.pattern_0value_1units, wavelength.toFixed( 0 ), strings.units_nm );
    }
  } );

  return WavelengthControls;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Scene graph for the 'Beer's Law' screen.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/view/BeersLawView',['require','BEERS_LAW_LAB/beerslaw/view/ATDetectorNode','BEERS_LAW_LAB/beerslaw/view/BeamNode','BEERS_LAW_LAB/beerslaw/view/BLLRulerNode','DOT/Bounds2','BEERS_LAW_LAB/beerslaw/view/CuvetteNode','PHET_CORE/inherit','BEERS_LAW_LAB/beerslaw/view/LightNode','SCENERY/nodes/Node','SCENERY_PHET/ResetAllButton','SCENERY/Scene','JOIST/ScreenView','BEERS_LAW_LAB/beerslaw/view/SolutionControls','BEERS_LAW_LAB/beers-law-lab-strings','SCENERY/nodes/Text','BEERS_LAW_LAB/beerslaw/view/WavelengthControls'], function( require ) {
  

  // imports
  var ATDetectorNode = require( 'BEERS_LAW_LAB/beerslaw/view/ATDetectorNode' );
  var BeamNode = require( 'BEERS_LAW_LAB/beerslaw/view/BeamNode' );
  var BLLRulerNode = require( 'BEERS_LAW_LAB/beerslaw/view/BLLRulerNode' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var CuvetteNode = require( 'BEERS_LAW_LAB/beerslaw/view/CuvetteNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LightNode = require( 'BEERS_LAW_LAB/beerslaw/view/LightNode' );
  var Node = require( 'SCENERY/nodes/Node' );
  var ResetAllButton = require( 'SCENERY_PHET/ResetAllButton' );
  var Scene = require( 'SCENERY/Scene' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var SolutionControls = require( 'BEERS_LAW_LAB/beerslaw/view/SolutionControls' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var Text = require( 'SCENERY/nodes/Text' );
  var WavelengthControls = require( 'BEERS_LAW_LAB/beerslaw/view/WavelengthControls' );

  /**
   * @param {BeersLawModel} model
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function BeersLawView( model, mvt ) {

    var thisView = this;
    ScreenView.call( thisView, { renderer: 'svg' } );

    var lightNode = new LightNode( model.light, mvt );
    var cuvetteNode = new CuvetteNode( model.cuvette, model.solution, mvt, 0.1 /* snapInterval, cm */ );
    var beamNode = new BeamNode( model.beam );
    var detectorNode = new ATDetectorNode( model.detector, model.light, mvt );
    var wavelengthControls = new WavelengthControls( model.solution, model.light );
    var rulerNode = new BLLRulerNode( model.ruler, mvt );
    var foregroundNode = new Node();
    var solutionControls = new SolutionControls( model.solutions, model.solution, foregroundNode );

    // Reset All button
    var resetAllButton = new ResetAllButton( function() {
      model.reset();
      wavelengthControls.reset();
    } );

    // Rendering order
    thisView.addChild( wavelengthControls );
    thisView.addChild( resetAllButton );
    thisView.addChild( solutionControls );
    thisView.addChild( detectorNode );
    thisView.addChild( cuvetteNode );
    thisView.addChild( beamNode );
    thisView.addChild( lightNode );
    thisView.addChild( rulerNode );
    thisView.addChild( foregroundNode ); // last, so that combo box list is on top

    // Layout for things that don't have a location in the model.
    {
      // below the light
      wavelengthControls.left = lightNode.left;
      wavelengthControls.top = lightNode.bottom + 20;
      // below cuvette
      solutionControls.left = cuvetteNode.left;
      solutionControls.top = cuvetteNode.bottom + 60;
      // bottom left
      resetAllButton.left = solutionControls.right + 20;
      resetAllButton.bottom = solutionControls.bottom;
    }
  }

  inherit( ScreenView, BeersLawView, { layoutBounds: new Bounds2( 0, 0, 1140, 700 ) } );

  return BeersLawView;
} );

define("image!BEERS_LAW_LAB/../images/Beers-Law-icon.jpg", function(){ var img = new Image();
window.phetImages = window.phetImages || []
window.phetImages.push(img);
img.src='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QCYRXhpZgAATU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAABIAAAAAQAAAEgAAAABAAWQAAAHAAAABDAyMTCgAAAHAAAABDAxMDCgAQADAAAAAf//AACgAgAEAAAAAQAAAiSgAwAEAAAAAQAAAXUAAAAA/+EC1mh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSfvu78nIGlkPSdXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQnPz4KPHg6eG1wbWV0YSB4bWxuczp4PSdhZG9iZTpuczptZXRhLyc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyc+CgogPHJkZjpEZXNjcmlwdGlvbiB4bWxuczpleGlmPSdodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyc+CiAgPGV4aWY6WFJlc29sdXRpb24+NzI8L2V4aWY6WFJlc29sdXRpb24+CiAgPGV4aWY6WVJlc29sdXRpb24+NzI8L2V4aWY6WVJlc29sdXRpb24+CiAgPGV4aWY6UmVzb2x1dGlvblVuaXQ+SW5jaDwvZXhpZjpSZXNvbHV0aW9uVW5pdD4KICA8ZXhpZjpFeGlmVmVyc2lvbj5FeGlmIFZlcnNpb24gMi4xPC9leGlmOkV4aWZWZXJzaW9uPgogIDxleGlmOkZsYXNoUGl4VmVyc2lvbj5GbGFzaFBpeCBWZXJzaW9uIDEuMDwvZXhpZjpGbGFzaFBpeFZlcnNpb24+CiAgPGV4aWY6Q29sb3JTcGFjZT5VbmNhbGlicmF0ZWQ8L2V4aWY6Q29sb3JTcGFjZT4KICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTQ4PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MzczPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KIDwvcmRmOkRlc2NyaXB0aW9uPgoKPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0ncic/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';
return img;});

// Copyright 2002-2013, University of Colorado Boulder

/**
 * The 'Beer's Law' screen. Conforms to the contract specified in joist/Screen.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define('BEERS_LAW_LAB/beerslaw/BeersLawScreen',['require','BEERS_LAW_LAB/beerslaw/model/BeersLawModel','BEERS_LAW_LAB/beerslaw/view/BeersLawView','SCENERY/nodes/Image','PHETCOMMON/view/ModelViewTransform2','BEERS_LAW_LAB/beers-law-lab-strings','DOT/Vector2','image!BEERS_LAW_LAB/../images/Beers-Law-icon.jpg'], function( require ) {
  

  // imports
  var BeersLawModel = require( 'BEERS_LAW_LAB/beerslaw/model/BeersLawModel' );
  var BeersLawView = require( 'BEERS_LAW_LAB/beerslaw/view/BeersLawView' );
  var Image = require( 'SCENERY/nodes/Image' );
  var ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' );
  var strings = require( 'BEERS_LAW_LAB/beers-law-lab-strings' );
  var Vector2 = require( 'DOT/Vector2' );

  // images
  var beersLawImage = require( 'image!BEERS_LAW_LAB/../images/Beers-Law-icon.jpg' );

  function BeersLawScreen() {

    this.name = strings.beersLaw;
    this.icon = new Image( beersLawImage );
    this.backgroundColor = 'white';

    // No offset, scale 125x when going from model to view (1cm == 125 pixels)
    var mvt = ModelViewTransform2.createOffsetScaleMapping( new Vector2( 0, 0 ), 125 );

    this.createModel = function() {
      return new BeersLawModel( mvt );
    };

    this.createView = function( model ) {
      return new BeersLawView( model, mvt );
    };
  }

  return BeersLawScreen;
} );
// Copyright 2002-2013, University of Colorado Boulder

/**
 * Main entry point for the 'Beer's Law Lab' sim.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
require( [ 'JOIST/SimLauncher', 'JOIST/Sim', 'BEERS_LAW_LAB/concentration/ConcentrationScreen', 'BEERS_LAW_LAB/beerslaw/BeersLawScreen', 'BEERS_LAW_LAB/beers-law-lab-strings' ],
  function( SimLauncher, Sim, ConcentrationScreen, BeersLawScreen, strings ) {
    

    //TODO i18n?
    var simOptions = {
      credits: 'PhET Development Team -\n' +
               'Lead Design: Julia Chamberlain\n' +
               'Software Development: Chris Malley\n' +
               'Design Team: Kelly Lancaster, Emily B. Moore, Ariel Paul, Kathy Perkins',
      thanks: 'Thanks -\n' +
              'Conversion of this simulation to HTML5 was funded by the Royal Society of Chemistry.'
    };

    // Appending '?dev' to the URL will enable developer-only features.
    if ( window.phetcommon.getQueryParameter( 'dev' ) ) {
      simOptions = _.extend( {
        // add dev-specific options here
        showHomeScreen: false,
        screenIndex: 0
      }, simOptions );
    }

    SimLauncher.launch( function() {
      var sim = new Sim( strings.beersLawLab, [ new ConcentrationScreen(), new BeersLawScreen() ], simOptions );
      sim.start();
    } );
  } );

define("beers-law-lab-main", function(){});

// Copyright 2002-2013, University of Colorado Boulder

/*
 * RequireJS configuration file for the 'Beer's Law Lab' sim.
 * Paths are relative to the location of this file.
 *
 * @author Chris Malley
 */

require.config( {

  deps: ['beers-law-lab-main'],

  config: {
    i18n: {
      locale: 'en_us'
    }
  },

  paths: {

    // third-party libs
    i18n: '../../sherpa/i18n-2.0.4',

    // PhET plugins
    image: '../../chipper/requirejs-plugins/image',

    // PhET libs, uppercase names to identify them in require.js imports
    ASSERT: '../../assert/js',
    AXON: '../../axon/js',
    DOT: '../../dot/js',
    JOIST: '../../joist/js',
    KITE: '../../kite/js',
    NITROGLYCERIN: '../../nitroglycerin/js',
    PHET_CORE: '../../phet-core/js',
    PHETCOMMON: '../../phetcommon/js',
    SCENERY: '../../scenery/js',
    SCENERY_PHET: '../../scenery-phet/js',
    SUN: '../../sun/js',

    // this sim
    BEERS_LAW_LAB: "."
  },

  urlArgs: new Date().getTime()  // cache buster to make browser refresh load all included scripts
} );

define("beers-law-lab-config", function(){});
}());