// ZIM js Interactive Media framework https://zimjs.com by Dan Zen http://danzen.com (c) 2018
// Also see https://zimjs.com/distill to minify only the functions in your app
// free to use - donations welcome of course! https://zimjs.com/donate

// global variable to hold default frame - used by other ZIM modules like physics, etc.
var zimDefaultFrame;


////////////////  ZIM WRAP  //////////////

// Zim Wrap creates global wrapper functions for less typing

// set var zon=false before calling zim scripts to hide script comments
if (typeof zon == "undefined") var zon = true; // comments from zim scripts
if (typeof zns == 'undefined') var zns = false; // require zim namespace
/*--
zog(item1, item2, etc.)         ~ log

zog
global function

DESCRIPTION
Short version of console.log()
to log the item(s) to the console.
Use F12 to open your Browser console.
zog is dedicated to Pragma (Madeline Zen) who was coding with Dr Abstract (Dan Zen) from the start

Note: If zon (comments on) is set to false before ZIM runs, then all zog() commands are turned off

EXAMPLE
zog("hello", circle.x); // logs these values to the console
END EXAMPLE

PARAMETERS
item1, item2 (optional), etc. - items (expressions) to log to the console

RETURNS items it is logging separated by a space if more than one
--*///+0
// reported a bug in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1280818
// that after FF 46 binding the console did not show file and line number
// this is fixed in FF 50 - quite the conversation this stirred
var zog = zon?console.log.bind(console):function(){};
//-0

/*--
zid(string)                     ~ id

zid
global function

DESCRIPTION
Short version of document.getElementById(string)
to access an HTML tag by its id.

EXAMPLE
zid("logo").addEventListener("click", function(){});
END EXAMPLE

PARAMETERS
string - the id of the tag you are wanting to access

RETURNS HTML tag with id of string or null if not found
--*///+1
function zid(s) {
	z_d("1");
	return document.getElementById(s);
} //-1

/*--
zss(string)                     ~ css

zss
global function

DESCRIPTION
Short version of document.getElementById(string).style
to access the style property of an HTML tag by the tag id.

EXAMPLE
zss("logo").margin = "10px";
END EXAMPLE

PARAMETERS
string - the id of the tag whose style you are wanting to access

RETURNS style property of HTML tag with id of string or undefined if not found
--*///+2
function zss(s) {
	z_d("2");
	if (document.getElementById(s)) {return document.getElementById(s).style;}
	else if (zon) zog("zim wrap - zss(): id not found");
} //-2

/*--
zgo(url, target, width, height, fullscreen, modal)         ~ go

zgo
global function

DESCRIPTION
Short version of either window.location.href or window.open
to open a link in the same window or a specified window.

EXAMPLE
zid("logo").addEventListener("click", function(){zgo("http://zimjs.com");});

// with a ZIM object:
var button = new Button();
button.center(stage);
button.on("click", function() {zgo("http://zimjs.com");});
END EXAMPLE

PARAMETERS
url - the link to use (Absolute, Relative or Virtual)
target - (default null) the string name of a window (tab) _blank for new window each time
width - (default null) width of window (use with fullscreen true)
height - (default null) height of window (use with fullscreen true)
fullscreen - (default null) not really full screen but rather opens in a new window not tab
modal - (default false) set to true to force user to close window

RETURNS null if opening in same window or reference to the window otherwise
--*///+3
function zgo(u,t,w,h,f,m) {
	z_d("3");
	if ((zot(t) && t != "") || t == "_self") {
		window.location.href = u;
	} else {
		var added = "";
		if (w) added += "width=" + w + ",";
		if (h) added += "height=" + h + ",";
		if (f) added += "fullscreen=yes,";
		if (m) added += "modal=yes,alwaysRaised=yes";
		return window.open(u,t,added);
	}
} //-3

/*--
zum(string)                     ~ num

zum
global function

DESCRIPTION
Takes the units off a string number.
Converts "10px" string from styles to number 10, for instance.
If there is no value then this will return 0.

EXAMPLE
// in HTML
<div id="logo" style="position:relative; left:10px">LOGO</div>

// in JavaScript
var left = zum(zss("logo").left); // converts 10px to the Number 10
left += 20; // adds 20 to 10
zss("logo").left = left + "px"; // assigns 30px to left style
END EXAMPLE

PARAMETERS
string - the string representation of a number eg. "10px"

RETURNS a Number
--*///+4
function zum(s) {
	z_d("4");
	if (zot(s)) return;
	return Number(String(s).replace(/[^\d\.\-]/g, ''));
} //-4

/*--
zot(value)                      ~ not

zot
global function

DESCRIPTION
Test to see if value has no value (value must exist as var or parameter)
or if value has been set to null.
Good for setting function defaults.
Really just asking if the value == null.
Often we forget exactly how to do this - it is tricky:
value === null, value == undefined, value == 0, !value DO NOT WORK.

EXAMPLE
if (zot(width)) width = 100;
// equivalent to
if (width == null) width = 100;
END EXAMPLE

PARAMETERS
value - a variable or parameter you want to see if there is no value assigned

RETURNS Boolean true if value does not exist
--*///+4.5
function zot(v) {
	return v==null; // both null and undefined match but not false or 0
}//-4.5

/*--
zop(e)                          ~ stop

zop
global function

DESCRIPTION
Stop event propagation to subsequently added existing listeners.
Must pass it e || window.event from your event function.
NOTE: this is not canceling the default action -
to cancel default action use e.preventDefault();

EXAMPLE
zid("button").addEventListener("click", function(e) {
	// do something
	zop(e||window.event);
});
END EXAMPLE

PARAMETERS
e - the event object from your event function
 	collect the event object as e and then pass in e || window.event

RETURNS null
--*///+5
function zop(e) {
	z_d("5");
	if (zot(e)) return;
	if (e.stopImmediatePropagation) e.stopImmediatePropagation();
	if (window.event) window.event.cancelBubble=true;
} //-5

/*--
zil()                           ~ still

zil
global function

DESCRIPTION
Stop keys from moving content - arrows, spacebar, pgup, pgdown, home, end.
Stop scroll wheel from moving content - scrolling the canvas for instance.
ZIM Frame does this in the full, fit and outside scale modes.
If not using Frame, then you can do this once at the start of your code.
Returns an array of references to three listeners: [keydown, wheel and DOMMouseScroll].
Use these to removeEventListeners.
The arrows, etc, still work but just not their default window behaviour.

EXAMPLE
// at the top of your code
var listenersArray = zil();
// key and mousewheel arrows, spacebar, etc.
// will have their default actions stopped until you remove the listeners:
// window.removeEventListener("keydown", listenersArray[0]); // etc.
END EXAMPLE

RETURNS an Array
--*///+6
function zil() {
	z_d("6");
	var a = function(e) {if (!e) e = event; if (e.keyCode && (e.keyCode >= 32 && e.keyCode <= 40)) e.preventDefault();}
	var b = function(e) {if (!e) e = event; e.preventDefault();}
	var c = b;
	window.addEventListener("keydown", a, {passive:false});
	window.addEventListener("wheel", b, {passive:false});
	window.addEventListener("DOMMouseScroll", c, {passive:false});
	return [a, b, c];
} //-6

/*--
zet(selector)                   ~ set

zet
global function

DESCRIPTION
Uses document.querySelectorAll() to get a list of tags.
Returns a ZIM Zet object which can be used to add events or styles to the set.

EXAMPLE
zet(".class").on("click", function(){}); // would add function event to all tags with the class
zet("p").css("color", "goldenrod"); // would make the text of all paragraphs goldenrod
zet("#test").css({color:"red", "backgound-color":"blue", paddingLeft:"20px"});

// set a custom open property on all section bars to false
zet("section .bar").prop("open", false);
// set the custom open property on all section bars to true and set the innerHTML to CLOSE
zet("section .bar").prop({open: true, innerHTML: "CLOSE"});
END EXAMPLE

PARAMETERS
selector -  a CSS query selector such as a class, id, tag, or multiple selectors separated by commands
	can also be complex selectors suchs as ".class img"

METHODS (on the returned Zet object)
zet(selector).on(type, function) - a shortcut for addEventListener() and will be added to all tags matching the selector
zet(selector).off(type, function) - a shortcut for removeEventListener() and will be remove from all tags matching the selector
zet(selector).css(property, value) - gets and sets styles
	- gets the first programmatic property if a single string property is passed
	- sets the property to the value on each of the Zet's tags from the selector passed to zet()
	- if an object of properties and values is passed as the single parameter then sets all these properties
	- NOTE: style names do not need quotes unless the dash is used - so camelCase does not require quotes
	- NOTE: remember that commas are used for objects - not the semi-colon as in CSS
zet(selector).prop(property, value) - gets or sets a property on a set of tags
	- if an object of properties and values is provided as a single parameter, then sets all these on the set
	- else if no value is set then returns an array of the set tags values for the property
	- else if value is a single value then sets the property of the tags in the set to the value

PROPERTIES  (on the returned Zet object)
tags - an HTML NodeList tag list

RETURNS Zet object with on(), off(), css() methods and tags property (NodeList tag list)
--*///+6.1
function zet(selector) {
	z_d("6.1");
	function Zet() {
		var that = this;
		this.on = function(type, call) {
			if (zot(selector) || zot(type) || zot(call)) return;
			var tags = that.tags;
			for (var i=0; i<tags.length; i++) {
				tags[i].addEventListener(type, call);
			}
		}
		this.off = function(type, call) {
			if (zot(selector) || zot(type) || zot(call)) return;
			var tags = that.tags;
			for (var i=0; i<tags.length; i++) {
				tags[i].removeEventListener(type, call);
			}
		}
		Object.defineProperty(that, 'tags', {
			get: function() {
				if (zot(selector)) return [];
				if (typeof selector == 'string' || selector instanceof String) {
					return document.querySelectorAll(selector);
				} else { // selector is already an object - assume a tag
					if (typeof (selector).innerHTML == "string") return [selector];
					return [];
				}
			},
			set: function(t) {
			}
		});
		this.css = function(property, value) {
			// if property is object then assign all props in object
			var tags = that.tags;
			for (var i=0; i<tags.length; i++) {
				if (arguments.length == 1 && arguments[0].constructor === {}.constructor) {
					for (var p in property) {
						tags[i].style[p] = property[p];
					}
				} else if (arguments.length == 1) {
					return that.tags[0].style[property];
				} else {
			    	tags[i].style[property] = value;
				}
			}
		}
		this.prop = function(property, value) {
			if (zot(property)) return;
			var tags = that.tags;
			var a = [];
			for (var i=0; i<tags.length; i++) {
				if (zot(value)) {
					if (property.constructor === {}.constructor) {
						for (var p in property) {
							tags[i][p] = property[p];
						}
					} else {
						a.push(tags[i][property]);
					}
				} else {
			    	tags[i][property] = value;
				}
			}
			if (zot(value)) return a;
		}
	}
	return new Zet();
} //-6.1

/*--
zob(func, args, sig, scope)     ~ object

zob
global function

DESCRIPTION
A system to build functions or classes that allow traditional parameters
or a configuration object passed in as a single parameter.
The configuration object has property names that match the function arguments.

To use zob on your own functions, pass in a function and the function's arguments
and insert zob into first line of your function as shown below.
Replace yourFunction with a reference to your function but keep arguments as is.

EXAMPLE
function test(a,b,c){
	var duo; if (duo = zob(test, arguments)) return duo;
};
test(1,null,3); // traditional parameters in order
test({a:1,c:3}); // configuration object with zob
END EXAMPLE

NOTE: if you are minifying the file then you need to do an extra step
add a string version of the signature of your function above the duo call
then pass the signature in as a parameter to zob()

EXAMPLE
function test(a,b,c){
	var sig = "a,b,c";
	var duo; if (duo = zob(test, arguments, sig)) return duo;
};
END EXAMPLE

NOTE: if you are running the function as a constructor with the new keyword
then you need to pass in this (keyword) as the last parameter (sig can be null)
this allows zob() to test to see if we need to rerun the function as a constructor

EXAMPLE
var duo; if (duo = zob(yourFunction, arguments, sig, this)) return duo;
END EXAMPLE

many of the ZIM functions and classes use this "DUO" technique
the documentation for parameters will tell you if they support DUO
works also with JS6 default parameter values

PARAMETERS
func - reference to the function you want to use params or a config object with
args - reference to the arguments property of the function (literally, use "arguments" with no quotes)
sig - (default null) a string listing of the parameters just how they are in the () not including the ()
	required if you are minifying the file as minifying changes the signature
scope - (default null) reference to this (litterally, use "this" without the quotes)
	required if the function is being run with the new keyword

RETURNS um... a Boolean
--*///+7
function isDUO(a) {return a.length == 1 && a[0] != undefined && a[0].constructor === {}.constructor;}
function zob(func, args, sig, scope) {
	var zimon = (zim && (zim.ZIMONON || window.ZIMONON));
	if (isDUO(args)) {
		z_d("7");
		var zp = args[0];
		var za = (zot(sig))?func.toString().split(/\n/,1)[0].match(/\((.*)\)/)[1].replace(/\s+/g,"").split(","):sig.replace(/\s+/g,"").split(",");
		var zv = []; var zi; var zt;
		for (zi=0; zi<za.length; zi++) {zt=za[zi].split("=")[0]; za[zi]=zt; zv.push(zp[zt]);}
		for (zi in zp) {if (za.indexOf(zi)<0) {if (zon) zog(func,"bad argument "+zi);}};
		var zr; if (zr=(func.prototype.isPrototypeOf(scope))?new (func.bind.apply(func,[null].concat(zv)))():func.apply(null,zv)) {if (zimon && !zr.arguments) {zr.arguments = [zp]}; return zr;} else {return true;}
	} else {
		if (zimon && scope && args && zot(scope.arguments)) {
			scope.arguments = Array.prototype.slice.call(args);
		}
	}
}//-7

/*--
zik(Array|function|object|Pick)      ~ pick

zik
global function

DESCRIPTION - DEPRECIATED as of ZIM 10
Has now been replaced with ZIM Pick() class which formalizes zik() and ZIM VEE into a more general class.
See ZIM Pick() under the Code module Classes.
--*///+7.5
function zik(v) {
	z_d("7.5");
	z_d("17.6");
	if (zot(v)) return;
	return zim.Pick.choose(v);
}//-7.5

/*--
zta(item1, item2, etc.)         ~ table

zta
global function

DESCRIPTION
Short version of console.table()
to log the Arrays or Objects to the console as a table
Use F12 to open your Browser console.

Note: if zon (comments on) is set to false before ZIM runs then all zta() commands will be turned off

EXAMPLE
zta(["will", "this", "wind"]); // logs as a table
END EXAMPLE

PARAMETERS
item1, item2 (optional), etc. - Arrays or Objects to log to the console

RETURNS items it is logging
--*///+7.6
var zta = console.table&&zon?console.table.bind(console):function(){};
//-7.6

// the above functions are global for quick usage
// start the zim module pattern - from here on, everything is stored on the zim namespace

var zim = function(zim) {

	// zim colors -- also stored on Frame object for legacy
	zim.orange	= "#f58e25";
	zim.green  	= "#acd241";
	zim.pink  	= "#e472c4";
	zim.blue   	= "#50c4b7";
	zim.brown  	= "#d1a170";
	zim.yellow  = "#ebcb35";
	zim.purple	= "#993399";
	zim.red 	= "#fb4758"; // dedicated to Alexa
	zim.silver	= "#999999";
	zim.tin		= "#777777";
	zim.grey   	= "#555555";
	zim.gray 	= "#555555";
	zim.lighter = "#eeeeee";
	zim.moon 	= "#dddddd";
	zim.light 	= "#cccccc";
	zim.dark 	= "#333333";
	zim.darker 	= "#111111";
	zim.black 	= "#000000";
	zim.white	= "#ffffff";
	zim.clear 	= "rgba(0,0,0,0)";
	zim.faint 	= "rgba(0,0,0,.01)";

////////////////  ZIM CODE  //////////////

// Zim Code adds some general code functionality along with Browser and DOM code
// some of these are common Web solutions over the years (sorry for lack of credit)

// SUBSECTION FEATURED

/*--
zim.shuffle = function(array)

shuffle
zim function

DESCRIPTION
Randomly shuffles elements of an array.
Actually changes the original array (and also returns it).

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var array = ["happy", "sad", "spooked"];
var randomFromArray = shuffle(array)[0];
// this will be randomized each time it is run
END EXAMPLE

EXAMPLE
var array = shuffle(["happy", "sad", "spooked"]);
for (var i=0; i<array.length) zog(array[i]);
// this will get random and unique elements of the array
END EXAMPLE

PARAMETERS
array - the Array to shuffle

RETURNS the modified Array
--*///+8
	zim.shuffle = function(array) {
		z_d("8");
		if (zot(array)) return;
		var i = array.length, j, temp;
		if (i == 0) return array;
		while(--i) {
			j = Math.floor(Math.random()*(i+1));
			temp=array[i];
			array[i]=array[j];
			array[j]=temp;
		}
		return array;
	}//-8

/*--
zim.rand = function(a, b, integer, negative)

rand
zim function

DESCRIPTION
Returns a random integer between and including a and b if integer is true.
Returns a random number (with decimals) including a and up to b but not b if integer is false.
b is optional and if left out will default to 0 (includes 0).
integer is a boolean and defaults to true.
If a and b are 0 then just returns Math.random().

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var speed = rand(10,20); // 10, 11, 12... 18, 19 or 20

var colors = ["blue", "yellow", "green"];
var color = colors[rand(colors.length-1)]; // note the length-1

// the equivalent of:
var color = colors[Math.floor(Math.random()*colors.length)];

// OR a technique often used without using rand():
var color = shuffle(colors)[0];

// here we get a speed that is either from 5 to 10 or -5 to -10
var speed = rand(5,10,null,true);
END EXAMPLE

PARAMETERS
a - the first Number for the range
	if a and b are not provided, rand() acts like Math.random()
	if parameter b is not provided, rand will use range 0 to and including a
b - (default 0) second Number for the range
	it does not matter if a>b or a<b
integer - (default true) set to false to include decimals in results
	if false, range will include decimals up to but not including the highest number
	if a or b have decimals this is set to false
negative - (default false) includes the negative range as well as the positive

RETURNS a Number
--*///+9
	zim.rand = function(a, b, integer, negative) {
		z_d("9");
		if (zot(a) && zot(b)) return Math.random();
		if (zot(a) || isNaN(a)) a = 0;
		if (zot(b) || isNaN(b)) b = 0;
		if (a%1!=0 || b%1!=0) integer = false;
		if (zot(integer)) integer = true;
		if (negative) if (Math.random()>.5) {a*=-1; b*=-1;};
		if (integer) if (a>b) {a++;} else if (b>a) {b++;}
		var r;
		if (a == 0 && b == 0) return 0;
		else if (b == 0) r = Math.random()*a;
		else r = Math.min(a,b) + Math.random()*(Math.max(a,b)-Math.min(a,b));
		if (integer) return Math.floor(r);
		else return r;
	}//-9

/*--
zim.series = function(array|item1, item2, item3)

series
zim function

DESCRIPTION
Returns a function that will return each value passed as a parameter (or an Array) in order
This goes in sequence each time the function is called
Use this to pass a series in to any ZIM VEE value so a looping series is obtained

NOTE: was called makeSeries() which is now depreciated

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// note - do not call the variable name series
var s = series(red, green, blue);
s(); // red
s(); // green
s(); // blue
s(); // red, etc.

// or
var colors = [red, green, blue];
var s = series(colors);
s(); // red
s(); // green
s(); // blue
s(); // red, etc.

new Tile(new Rectangle(10,10,[blue, red]), 10, 10); would randomize colors
new Tile(new Rectangle(10,10,series(blue, red)), 10, 10); would alternate colors
END EXAMPLE

EXAMPLE
STYLE = {color:series(pink, green, blue)}
loop(9, function (i) {
	new Circle(100).loc(110+i*100, 400)
});
END EXAMPLE

PARAMETERS
array|item1 - the first item - or an array of results that will be called in order as the resulting function is called
    // when used with ZIM VEE - the values may be further ZIM VEE values (including more series values)
item2 - the second item if the first is not an array
item3 - the third item, etc. to as many items as needed

PROPERTIES
array - an array of items passed in to the function

RETURNS a function that can be called many times - each time returning the next value in the series
--*///+13.61
	zim.series = function() {
		z_d("13.61");
		var array;
		if (arguments.length == 0) return function(){};
		if (arguments.length == 1 && Array.isArray(arguments[0])) array = arguments[0];
		else array = arguments;
        var count = 0;
        var f = function() {
            return array[(count++)%array.length];
        }
        f.array = array;
		f.type = "series";
        return f;
	}//-13.61

/*--
zim.makeSeries = function(array)
	depreciated - use series()
--*///+13.6
	zim.makeSeries = function(array) {
		z_d("13.6");
        if (zot(array)) return function(){};
        var count = 0;
        var f = function() {
            return array[(count++)%array.length];
        }
        f.array = array;
        return f;
	}//-13.6

/*--
zim.loop = function(obj, call, reverse, step, start, end)

loop
zim function

DESCRIPTION
1. If you pass in a Number for obj then loop() does function call that many times
and passes function call the currentIndex, totalLoops, startIndex, endIndex, obj.
By default, the index starts at 0 and counts up to one less than the number.
So this is like: for (var i=0; i<obj; i++) {}

2. If you pass in an Array then loop() loops through the array
and passes the function call the element in the array, currentIndex, totalLoops, startIndex, endIndex and the array.
So this is like: for (var i=0; i<obj; i++) {element = array[i]}

3. If you pass in an Object literal then loop() loops through the object
and passes the function call the property name, value, currentIndex, totalLoops, startIndex, endIndex, obj
So this is like: for (var i in obj) {property = i; value = obj[i];}

4. If you pass an HTML NodeList or HTMLCollection then loop() loops and gives each tag in the NodeList or HTMLCollection
see ZIM zet() for an example of getting a NodeList (like the $() in JQuery)

5. See also the loop method under ZIM Methods to see how to loop through a ZIM Container

NOTE: If you pass in true for reverse, the loop is run backwards counting to 0 (by default)
NOTE: use return to act like a continue in a loop and go to the next loop
NOTE: return a value to return out of the loop completely like a break (and return a result if desired)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var container = new Container();
loop(1000, function(i) { // gets passed an index i, totalLoops 1000, startIndex 0, endIndex 999, obj 1000
	// make 1000 rectangles
	container.addChild(new Rectangle());
});
stage.addChild(container);

// to continue or break from loop have the function return the string "continue" or "break"
loop(10, function(i) {
	if (i%2==0) return; // skip even
	if (i>6) return "break"; // quit loop when > 6
	zog(i);
});

var colors = [green, yellow, pink];
loop(colors, function(color, index, start, end, array) { // do not have to collect all these
	zog(color); // each color
});

var person = {name:"Dan Zen", occupation:"Inventor", location:"Dundas"}
var result = loop(person, function(prop, val, index, total, start, end, object) { // do not have to collect all these
	zog(prop, val); // each key value pair
	if (val == "criminal") return "criminal"; // this would return out of the loop to the containing function
});
if (result == "criminal") alert("oh no!");

var tags = zet(".heading").tags; // get an NodeList of tags styled with heading class
loop(tags, function(tag, i) {
	tag.innerHTML = i + ". " + tag.innerHTML; // add an index number in front
});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
obj - a Number of times to loop or an Array or Object or NodeList to loop through
call - the function to call
	the function will receive (as its final parameters) the index, total, start, end, obj
		where the index is the current index, total is how many times the loop will run
		start is the start index, end is the end index and obj is the object passed to the loop
	the starting parameters vary depending on the type of obj:
	if the obj is a number then the first parameter is the index (no extra starting parameters given)
	if the obj is an array then the first parameter is the element at the current index
	if the obj is an object literal then the first and second parameters are the property name and property value at the current index
reverse - (default false) set to true to run the loop backwards to 0
step - (default 1) each step will increase by this amount (positive whole number - use reverse to go backwards)
start - (default 0 or length-1 for reverse) index to start
end - (default length-1 or 0 for reverse) index to end

RETURNS any value returned from the loop - or undefined if no value is returned from a loop
--*///+9.5
	zim.loop = function(obj, call, reverse, step, start, end) {
		var sig = "obj, call, reverse, step, start, end";
		var duo; if (duo = zob(zim.loop, arguments, sig)) return duo;
		z_d("9.5");
		if (zot(obj) || zot(call)) return undefined;
		if (zot(reverse)) reverse = false;
		if (zot(step) || step <= 0) step = 1;

		var type = typeof obj=="number"?"number":(obj.constructor === Array?"array":(obj.constructor === {}.constructor?"object":(obj instanceof NodeList?"nodelist":(obj instanceof HTMLCollection?"htmlcollection":"invalid"))));

		if (type == "invalid") {
			return undefined;
		}
		if (type == "number" || type == "array" || type == "nodelist" || type == "htmlcollection") {
			var length = type=="number"?obj:obj.length;
			var total = getTotal(length-1);
			if (total == 0) return;
			if (reverse) {
				for(var i=start; i>=end; i-=step) {
					if (type=="number") {
						var r = call(i, total, start, end, obj);
					} else if (type=="array") {
						var r = call(obj[i], i, total, start, end, obj);
					} else { // nodelist
						var r = call(obj.item(i), i, total, start, end, obj);
					}
					if (typeof r != 'undefined') return r;
				}
			} else {
				for(var i=start; i<=end; i+=step) {
					if (type=="number") {
						var r = call(i, total, start, end, obj);
					} else if (type=="array") {
						var r = call(obj[i], i, total, start, end, obj);
					} else { // nodelist or htmlcollection
						var r = call(obj.item(i), i, total, start, end, obj);
					}
					if (typeof r != 'undefined') return r;
				}
			}
		} else if (type == "object") {
			var length = 0;
			var props = [];
			for (var i in obj) {
				length++;
				props.push(i);
			}
			var total = getTotal(length-1);
			if (total == 0) return;
			if (reverse) {
				for(var i=start; i>=end; i-=step) {
					var r = call(props[i], obj[props[i]], i, total, start, end, obj);
					if (typeof r != 'undefined') return r;
				}
			} else {
				for(var i=start; i<=end; i+=step) {
					var r = call(props[i], obj[props[i]], i, total, start, end, obj);
					if (typeof r != 'undefined') return r;
				}
			}
		}
		function getTotal(max) {
			if (zot(start)) start = reverse?max:0;
			if (zot(end)) end = reverse?0:max;
			if ((reverse && end > start) || (!reverse && start > end)) return 0;
			if ((start < 0 && end) <0 || (start > max && end > max)) return 0;
			start = Math.max(0, Math.min(start, max));
			end = Math.max(0, Math.min(end, max));
			return Math.floor((reverse?(start-end):(end-start)) / step) + 1;
		}
	}//-9.5

/*--
zim.timeout = function(time, call)

timeout
zim function

DESCRIPTION
Calls a function after the time delay - like window.setTimeout()
Uses window.requestAnimationFrame() that tends to rest when the window is not showing

NOTE: setTimeout has the time parameter last, timeout has it first
so that it is consistent with loop() and the CreateJS on() method

NOTE: to clear a timeout you use returnID.clear() - different than window.clearTimeout(returnID)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
timeout(1000, function(){
	circle.x += 100;
	stage.update();
});
// moves the circle 100 pixels after one second

// GAME to press button within one second:
var timeout = timeout(1000, function() {
	zog("you lose!");
	button.enabled = false;
});
var button = new Button().center(stage);
button.on("click", function() {
	zog("you win!");
	timeout.clear();
});
END EXAMPLE

PARAMETERS
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
time - |ZIM VEE| milliseconds to wait until function is called
call - function to call when the time passes - will receive the id object as a single parameter

RETURNS a ZIM timeoutObject to pause and clear the timeout with the following methods and properties:

METHODS - of ZIM timeoutObject
pause(state) - (default true) will pause the timeout - set to false to unpause the timeout
clear() - will clear the timeout

PROPERTIES - of ZIM timeoutObject
time - the time in milliseconds that has lapsed
paused - the paused state of the timeout
done - true if finished
--*///+9.7
	zim.timeout = function(time, call) {
		z_d("9.7");
		if (zot(call)) return;
		if (typeof call != 'function') return;
		if (zot(time)) time = 1000;
		time = zim.Pick.choose(time);
		var obj = {startTime:Date.now(), time:0, paused:false, done:false};
		var lastTime = obj.startTime;
		function next() {
			var now = Date.now()
			obj.time += now - lastTime;
			lastTime = now;
			if (obj.time >= time) {
				obj.done = true;
				(call)(obj);
				obj.clear();
				return;
			}
			obj.rid = requestAnimationFrame(next);
		}

		obj.pause = function(state) {
			if (zot(state)) state = true;
			if (state) { // pausing
				cancelAnimationFrame(obj.rid);
			} else { // unpausing
				lastTime = Date.now();
				next();
			}
			obj.paused = state;
		}

		obj.clear = function() {
			if (obj) cancelAnimationFrame(obj.rid);
			for (var i in obj) {
				delete obj[i];
			}
			obj.pause = function() {};
			obj.clear = function() {};
		}
		next(); // thanks StevenWarren for the glitch fix!
		return obj;
	}//-9.7

/*--
zim.interval = function(time, call, total, immediate)

interval
zim function

DESCRIPTION
Calls a function after each time delay - like window.setInterval().
Can pass in an Array of two times to set random time delays each interval.
Can pass in how many times you want to run the function and whether it runs right away.
Uses window.requestAnimationFrame() that tends to rest when the window is not showing.

NOTE: setInterval has the time parameter last, interval has it first
so that it is consistent with loop() and the CreateJS on() method

NOTE: to clear a interval you use intervalObj.clear() - different than window.clearInterval(returnID)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
interval(1000, function(){
	circle.x += 100;
	stage.update();
});
// every second the circle will move 100 pixels
// if you want smooth movement, use:

Ticker.add(function() {
	circle.x += 100; // no need for stage.update()
});

interval(1000, function(obj) {
	zog("counting " + obj.count); // starts counting at 1
	if (obj.count == 10) obj.clear(); // will now log 1 to 10
});
OR better:
interval(1000, function(obj) {
	zog("counting " + obj.count); // starts counting at 1
}, 10); // now will log 1 - 10 with total parameter set to 10

IMMEDIATE:
interval(1000, function(obj) {
	zog("counting " + obj.count); // starts counting at 0
}, 10, true); // now will log 0 - 9 with immediate parameter set to true

EXTERNAL control:
var interval = interval(1000, function() {
	zog("counting " + interval.count); // starts counting at 1
});
var button = new Button({label:"STOP", toggle:"START"}).center(stage);
button.on("click", function(){interval.pause(button.toggled);});

RANDOM intervals with ZIM Pick() literals
interval({min:200, max:800}, dropBombs); // bombs will fall at different rates between 200ms and 800ms
interval([1000, 2000], dropBombs); // bombs will fall at either 1000 or 2000 ms
var count = 1;
function increase() {return ++count*1000}
interval(increase, dropBombs); // bombs will fall at 1000, then again after 2000 more ms and 3000 ms more after that, etc.
END EXAMPLE

PARAMETERS
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
time - |ZIM VEE| (default 1000) milliseconds for the interval (delay until the function runs - again and again)
call - function to call when the interval passes
	Will be passed a ZIM intervalObject as a single parameter
	This is the same as the return object from animate()
	See the Returns section below for methods and properties of the intervalObject
total - (default null - infinite) the number of times the function is called
	note: the count property counts intervals but the total property is based on function calls.
	The total will be equal to the end count with the immediate parameter set to false (default)
	but the total will be one less than the count if the immediate parameter is true (like an Array index and length)
immediate - (default false) set to true to call the function right away (and then still call every interval)
	This will not increase the count in the intervalObject because count counts intervals not function calls
	Use the provided parameter of the call function to access the intervalObject inside the call function

RETURNS a ZIM intervalObject to pause and clear the interval with the following methods and properties:

METHODS - of ZIM intervalObject
pause(state, immediate) - (default true) will pause the interval - set to false to unpause the interval
	immediate will make the interval function run right away when unpausing (no effect when pausing)
clear() - will clear the interval

PROPERTIES - of ZIM intervalObject
time - |ZIM VEE| get or set the time for the interval (see time parameter)
count - get the number of times the interval has run (if immediate is true, the first count is 0)
total - get or set the number of times the interval will run if the total parameter is set - otherwise -1 for infinite
paused - get the paused state of the interval (see pause() method)
pauseTimeLeft - if paused, get how much time is left once unpaused
--*///+9.8
	zim.interval = function(time, call, total, immediate) {
		z_d("9.8");
		if (zot(call)) return;
		if (typeof call != 'function') return;
		if (zot(time)) time = 1000;
		if (zot(immediate)) immediate = false;
		if (!zot(total) && (isNaN(total) || total<=0)) return;
		if (zot(total)) total = -1;
		var obj = {count:0, total:total, paused:false, time:time, active:true};


		function interval() {
			obj.startTime = Date.now();
			obj.interval = zim.Pick.choose(obj.time);
			obj.id = setTimeout(function() {
				if (obj.paused) return;
				if (!obj.active) return;
				obj.rid = requestAnimationFrame(interval);
				obj.count++;
				(call)(obj);
				checkTotal();
			}, obj.interval);
		}
		if (immediate) {
			setTimeout(function() {
				(call)(obj);
				checkTotal();
			}, 10);
		}
		function checkTotal() {
			if (total == -1) return;
			if (obj.count >= (immediate?obj.total-1:obj.total)) obj.clear();
		}
		var pausedTimeout;
		obj.pause = function(state, immediate) {
			if (zot(state)) state = true;
			if (state) { // pausing
				clearTimeout(pausedTimeout);
				clearTimeout(obj.id);
				cancelAnimationFrame(obj.rid);
				obj.pauseTimeLeft = obj.interval - (Date.now()-obj.startTime);
			} else { // unpausing
					pausedTimeout = setTimeout(function() {
						obj.count++;
						(call)(obj);
						interval();
						checkTotal();
					}, immediate?0:obj.pauseTimeLeft);
				obj.pauseTimeLeft = null;
			}
			obj.paused = state;
		}
		obj.clear = function() {
			obj.active = false;
			clearTimeout(pausedTimeout);
			cancelAnimationFrame(obj.rid);
			clearTimeout(obj.id);
			var count = obj.count;
			for (var i in obj) {
				delete obj[i];
			}
			obj.active = false;
			obj.count = count;
			obj.pause = function() {};
			obj.clear = function() {};
		}
		interval();
		return obj;
	}//-9.8

/*--
zim.async = function(url, callback)

async
zim function

DESCRIPTION
A way to send data back and forth to a server script without reloading the HTML page.
(like AJAX but without the bother)
Uses a dynamic script call with an optional callback (cross domain calls are okay)
also known as JSON-P pattern but JSON is unnecessary - note, no JSON in the examples below.
Pass a url to the server script (ie. php or node page)
and an optional callback function that you define in your code (cannot be an anonymous function).
async will automatically add a random number to the end of your script call to defeat cache.

NOTE: async uses GET so data is limited to GET length (as of ZIM 10 - this is 2K to 8K depending on Browser)
If more data is required, use an AJAX library

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// existing service:
// assuming that we have a callback function called test as shown below
async("http://ip-api.com/json?callback=async.test",test);
function test(data) {zog(data.country);}
// note that the callback we pass the service is async.test not just test
// this allows zim to handle scope issues and garbage collect the dynamic script when done
// if the service passes JSON you may need to JSON.decode() the data being returned
// this service passes an object literal not JSON despite its file name
END EXAMPLE

EXAMPLE
// CLIENT - your own server script:
// assuming we have a callback function called myFunction as shown below
async("http://yourserver.com/script.php?id=72&name=dan", myFunction);
function myFunction(data){zog(data);}

// SERVER - your script must output the following format as a string:
// "async.myFunction(somedata)"
// in the php file we would use:
header('Content-type: text/javascript');
echo "async.myFunction('success')";
// to return an object literal with nodejs express for example, you would use:
res.send('async.myFunction({list:[1,2,3], name:"whatever"})');
// the data parameter in the myFunction function defined earlier would be an object literal
// we could then do zog(data.list[0]) to log the value 1, etc.
END EXAMPLE

PARAMETERS
url - url to the server script (ie. php or node page)
callback - (default null) callback function that you define in your code (cannot be an anonymous function)

calling the return function on async does two things:
1. it handles scope issues so we can find your callback function
2. it handles garbage collection to remove the dynamic script tag that was used
if you do not specify a callback function then just send "" back from your server script
NOTE: we have experienced duplicate script calls if nothing is sent back

RETURNS undefined
--*///+29
	zim.async = function (url, callback) {
		z_d("29");
		if (zot(url)) return;
		var tag = document.createElement("script");
		if (callback) {
			var n = callback.toString().split(/\n/,1)[0].match(/^function\s?([^\s(]*)/)[1];
			// create callback bridge on async function object
			zim.async[n] = function() { // closure to access tag on callback bridge
				var t = tag;
				return function(d){
					// remove the script tag and do the callback
					if (t) t.parentNode.removeChild(t); t = null;
					callback(d);
				}
			}();
		} else {
			if (zim.async.z_s && zim.async.z_s.parentNode) zim.async.z_s.parentNode.removeChild(zim.async.z_s); // keep overwriting same script tag if no callback
			zim.async.z_s = tag;
		}
		if (!url.match(/\?/)) url += "?";
		tag.setAttribute("src", url + "&r="+Math.random());
		document.getElementsByTagName("head")[0].appendChild(tag);
	}//-29

/*--
zim.convertColor = function(color, toColorType, alpha)

convertColor
zim function

DESCRIPTION
Converts color to hex numbers - for example: "#333333"
Or converts color to HTML string - for example: "red"
Or converts color to RGB - for example: "rgb(0,0,0)"
Or converts color to RGBA - for example: "rgba(0,0,0,.5)"

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var color = convertColor("red"); // result is "#ff0000"
var color = convertColor("#ff0000", "string"); // result is "red"
var color = convertColor("f00", "string"); // result is "red" - note missing # okay and can use three digits
var color = convertColor(blue, "rgba", .5); // result is "rgba(80,196,183,0.5)"
var color = convertColor("rgb(256,256,0)", "rgba", .3); // result is ""rgba(256,256,0,.3)"
var color = convertColor("rgba(0,0,0,.2)", anyType, .5); // result is ""rgba(0,0,0,.5)"
END EXAMPLE

PARAMETERS
color - (default black) the HTML string or hex color (case insensitive)
	"rgb()" in only works with "rgba" out else returns rgb() value
	"rgba()" in only returns same thing with rgba() alpha set to alpha parameter
toColorType - (default "hex") or use "string", "rgb" or "rgba"
alpha - (default 1) the alpha used for the "rgba" toColorType

RETURNS a String with the converted color or black if a match is not found
--*///+27.5
	zim.convertColorCheck = false;
	zim.convertColor = function(color, toColorType, alpha) {
		if (!zim.convertColorCheck) {z_d("27.5"); zim.convertColorCheck=true;}
		if (zot(toColorType)) toColorType = "hex";
		if (zot(alpha)) alpha == 1;
		if (zot(color)) return;
		if (color.match(/rgba\(/)) {
			var c = color.split(",");
			c[3] = alpha+")";
			return c.join(",");
		}
		if (color.match(/rgb\(/)) {
			if (toColorType == "rgba") {
				var c = color.split(")");
				return c[0]+","+alpha+c[1];
			} else return color;
		}

		if (toColorType == "rgb" || toColorType == "rgba") {
			function hexToRgbA(hex){ // kennebec on StackOverflow
				var c;
				if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)){
					c= hex.substring(1).split('');
					if(c.length== 3){
						c= [c[0], c[0], c[1], c[1], c[2], c[2]];
					}
					c= '0x'+c.join('');
					if (toColorType == "rgb") {
						return 'rgb('+[(c>>16)&255, (c>>8)&255, c&255]+')';
					} else {
						return 'rgba('+[(c>>16)&255, (c>>8)&255, c&255].join(',')+','+alpha+')';
					}

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

/*--
zim.colorRange = function(color1, color2, ratio)

colorRange
zim function

DESCRIPTION
Gets the color in a range between two colors based on a ratio from 0-1
Used internally by setColorRange() method and colorRange property of ZIM shapes
including animating color from current color to a new color

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
zog(colorRange(green, blue, .5)); // #7ecb7c
var rect = new Rectangle(100,100,red).center().setColorRange(purple);
rect.colorRange = .1; will change color to #f1455e (closer to red than purple)
rect.animate({color:purple}, 1000); // will animate color to purple in one second
rect.wiggle("colorRange", .5, .2, .5, 1000, 5000); // wiggles the color in the range
END EXAMPLE

PARAMETERS
color1 - (default null) the first color as an HTML string or hex color (case insensitive)
color2 - (default black) the second color as an HTML string or hex color (case insensitive)
ratio - (default .5) the ratio where 0 is the first color and 1 the second color

RETURNS a hex color string
--*///+27.6
	zim.colorRangeCheck = false;
	zim.colorRange = function(color1, color2, ratio) {
		ratio = Math.max(0, Math.min(1, ratio));
		if (!zim.colorRangeCheck) {z_d("27.6"); zim.colorRangeCheck=true;}
		// thanks Chris Dolphin - StackOverflow
		// modified by Dan Zen to use hex input and output
		// possibly converting and converting back - but not quite...
		if (zot(color1)) color1 = "white";
		if (zot(color2)) color2 = "black";
		var c1 = zim.convertColor(color1, "rgb");
		var c2 = zim.convertColor(color2, "rgb");
		var color1 = c1.substring(4, c1.length - 1).split(',');
		var color2 = c2.substring(4, c2.length - 1).split(',');
		var difference;
		var newColor = "#";
		var c;
		for (var i=0; i<color1.length; i++) {
			difference = color2[i] - color1[i];
			c = Math.floor(parseInt(color1[i], 10) + difference * ratio).toString(16);
			if (c.length < 2) c = "0"+c;
			newColor += c;
		}
		return newColor;
	}//-27.6


/*--
zim.pointAlongCurve = function(points, ratio, getAngle)

pointAlongCurve
zim function

DESCRIPTION
Finds a point along a cubic Bezier curve - such as that used in Blob and Squiggle
as well as the Shape.graphics.bezierCurveTo() or tiny api bt()
Used internally for animating along Blob and Bezier curves

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// point1, control1, control2, point2
var points = [{x:100,y:100}, {x:200,y:100}, {x:200,y:100}, {x:200,y:200}]
var shape = new Shape().addTo();
shape.graphics
	.s("black").ss(2)
	.mt(points[0].x,points[0].y)
	.bt(points[1].x,points[1].y, points[2].x,points[2].y, points[3].x,points[3].y);
new Circle(10,red).loc(pointAlongCurve(points, .2));
END EXAMPLE

PARAMETERS
points - an array of point objects (or objects with an x and y property)
	for a cubic Bezier - point1, control1, control2, point2
ratio - (default .5) the ratio where 0 is at the first point and 1 is at the second point
getAngle - (default false) request a calculated angle of tangent at point

RETURNS a point object with x and y properties on the curve at the ratio
	as well as an angle property for the tangent if getAngle is true
--*///+27.7
	zim.pointAlongCurve = function(points, ratio, getAngle) {
		z_d("27.7");
		// Thanks markE - StackOverflow
		if (!points || !points[0] || !points[1] || !points[2] || !points[3]) return;
		var x=CubicN(ratio,points[0].x,points[1].x,points[2].x,points[3].x);
		var y=CubicN(ratio,points[0].y,points[1].y,points[2].y,points[3].y);
		if (getAngle) {
			var nextRatio = ratio+.05;
			if (nextRatio > 1) {
				ratio -= .05;
				nextRatio -= .05;
				// redo the ratio
				var x0=CubicN(ratio,points[0].x,points[1].x,points[2].x,points[3].x);
				var y0=CubicN(ratio,points[0].y,points[1].y,points[2].y,points[3].y);
			} else {
				var x0 = x;
				var y0 = y;
			}
			var x2 = CubicN(nextRatio,points[0].x,points[1].x,points[2].x,points[3].x);
			var y2 = CubicN(nextRatio,points[0].y,points[1].y,points[2].y,points[3].y);
			var angle = zim.angle(x0, y0, x2, y2);
			return({x:x,y:y,angle:angle});
		}
		return({x:x,y:y});
	}
	// cubic helper formula at percent distance
	function CubicN(pct, a,b,c,d) {
		var t2 = pct * pct;
		var t3 = t2 * pct;
		return a + (-a * 3 + pct * (3 * a - a * pct)) * pct
		+ (3 * b + pct * (-6 * b + b * 3 * pct)) * pct
		+ (c * 3 - c * 3 * pct) * t2
		+ d * t3;
	}//-27.7

/*--
zim.distanceAlongCurve = function(points)

distanceAlongCurve
zim function

DESCRIPTION
Finds approximate distance along a cubic Bezier curve - such as that used in Blob and Squiggle
as well as the Shape.graphics.bezierCurveTo() or tiny api bt()
Used internally for animating along Blob and Bezier curves

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// point1, control1, control2, point2
var points = [{x:100,y:100}, {x:200,y:100}, {x:200,y:100}, {x:200,y:200}]
var shape = new Shape();
shape.graphics
	.s("black").ss(2)
	.mt(points[0].x,points[0].y)
	.bt(points[1].x,points[1].y, points[2].x,points[2].y, points[3].x,points[3].y);
zog(distanceAlongCurve(points)); // 170.7
END EXAMPLE

PARAMETERS
points - an array of point objects (or objects with an x and y property)
	for a cubic Bezier - point1, control1, control2, point2

RETURNS an approximate distance along the curve
--*///+27.8
	zim.distanceAlongCurve = function(points) {
		z_d("27.8");
		// Thanks David F. Knight in OpenGl.org forumn
		// points are [startPt, controlPt1, controlPt2, endPt]
		var chord = zim.dist(points[0], points[3]);
		var controlDist = zim.dist(points[0], points[1]) + zim.dist(points[1], points[2]) + zim.dist(points[2], points[3]);
		return (chord + controlDist)/2;
	}//-27.8

/*--
zim.closestPointAlongCurve = function(point, segmentPoints, num, interpolate, percentage)

closestPointAlongCurve
zim function

DESCRIPTION
Finds the closest point along a cubic Bezier curve before the given point.
Blob and Squiggle use cubic Bezier as does the Shape.graphics.bezierCurveTo() or tiny api bt()
Used internally for adding points to a Blob and Bezier curves

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var blob = new Blob().center();
var points = blob.segmentPoints;
stage.on("stagemousedown", function (e) {
	var point = blob.globalToLocal(e.stageX, e.stageY)
	zog(closestPointAlongCurve({x:point.x, y:point.y}, points))
	// gives index of point on curve before mouse location
});
END EXAMPLE

PARAMETERS
point - an object with an x and y property
	this could be {x:100, y:140} or circle, etc.
	the results tell which segment to add the point to
	the segment starting with the returned index
segmentPoints - an array of cubic Bezier point data
	each being an array of points for a cubic Bezier
	in the format of [point1, control1, control2, point2]
	Note, this is not the same as Blob or Squiggle points
	but rather use the segmentPoints property of Blob and Squiggle
num - (default 10) the number of points per segment used to calculate answer
interpolate - (default false) will return closest test point - not index of closest existing point
percentage - (default false) will return percent (0-100) the nearest point is on the path (overrides interpolate)
RETURNS the index of the closest point in segmentPoints before the given point
	or if interpolate is true, will return the closest testPoint (use higher num for better result)
	or if percentage is true, will return percent (0-100) the nearest point is on the path (overrides interpolate)
--*///+27.9
	zim.closestPointAlongCurve = function(point, segmentPoints, num, interpolate, percentage) {
		z_d("27.9");
		var closest = 10000000;
		var closestTestPoint;
		var index = 0;
		var secondaryIndex = 0;
		if (zot(num)) num = 10;
		zim.loop(segmentPoints, function(points, i, t) {
			// add num more points to estimate closest
			zim.loop(num, function (j, total) {
				// var d = zim.dist(point, zim.pointAlongCurve(segmentPoints(that.points[i], that.points[i<t-1?i+1:0]), j/10));
				var testPoint = zim.pointAlongCurve(points, j/total);
				var d = zim.dist(point, testPoint);
				if (d < closest) {
					closest = d;
					closestTestPoint = testPoint
					index = i;
					secondaryIndex = j;
				}
			});
		});
		if (percentage) {
			return (index*num+secondaryIndex)/(segmentPoints.length*num)*100;
		} else if (interpolate) {
			return closestTestPoint;
		}
		return index;

	}//-27.9

/*--
zim.transformPoints = function(points, transformType, amount, x, y)

transformPoints
zim function

DESCRIPTION
Scales, rotates, or moves points about provided x and y - or 0, 0 if x and y are not provided
Used internally by Squiggle and Blob transformPoints method

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// from https://zimjs.com/nio/paths.html
var points = [[0,75,0,0,-150,150,150,-150],[300,75,0,0,0,0,0,0,"none"]];
var newPoints = transformPoints(points, "scale", 2);
// [[0,150,0,0,-300,300,300,-300],[600,150,0,0,0,0,0,0,"none"]]
END EXAMPLE

EXAMPLE
// or used with Squiggle:
var points = [[0,75,0,0,-150,150,150,-150],[300,75,0,0,0,0,0,0,"none"]];
var squiggle = new Squiggle({points:points}).transformPoints("scale", 2);
// a squiggle with points twice as big as before
END EXAMPLE

PARAMETERS
points - an array of points in the Squiggle and Blob format (controlType is left as is)
	[[controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType], [etc]]
transformType - String any of: "scale", "scaleX", "scaleY", "rotation", "x", "y"
amount - the amount to transform
x, y - (default 0, 0) the x and y position to transform about

RETURNS an array of points with numbers transformed
--*///+27.95
	zim.transformPoints = function(points, transformType, amount, x, y) {
		z_d("27.95");
		if (zot(points) || !Array.isArray(points)) return;
		if (zot(x)) x = 0;
		if (zot(y)) y = 0;
		var points = zim.copy(points);
		var xStart = x;
		var yStart = y;
		if (transformType == "rotation") {
			if (x != 0) points = zim.transformPoints(points, "x", -xStart);
			if (y != 0) points = zim.transformPoints(points, "y", -yStart);
		}
		var point;
		for (var i=0; i<points.length; i++) {
			point = points[i];
			if (!Array.isArray(point)) continue;
			// [[controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType], [etc]]
			if (transformType == "x") {
				point[0] += amount;
			} else if (transformType == "y") {
				point[1] += amount;
			} else if (transformType == "scaleX") {
				point[0] = (point[0]-x)*amount+x;
				point[4] = (point[4])*amount;
				point[6] = (point[6])*amount;
			} else if (transformType == "scaleY") {
				point[1] = (point[1]-y)*amount+y;
				point[5] = (point[5])*amount;
				point[7] = (point[7])*amount;
			} else if (transformType == "scale") {
				point[0] = (point[0]-x)*amount+x;
				point[4] = (point[4])*amount;
				point[6] = (point[6])*amount;
				point[1] = (point[1]-y)*amount+y;
				point[5] = (point[5])*amount;
				point[7] = (point[7])*amount;
			} else if (transformType == "rotation") {
				var a = amount*Math.PI/180;
				var x1 = point[0];
				var y1 = point[1];
				point[0] = x1*Math.cos(a) - y1*Math.sin(a);
				point[1] = y1*Math.cos(a) + x1*Math.sin(a);

				x1 = point[4];
				y1 = point[5];
				point[4] = x1*Math.cos(a) - y1*Math.sin(a);
				point[5] = y1*Math.cos(a) + x1*Math.sin(a);

				x1 = point[6];
				y1 = point[7];
				point[6] = x1*Math.cos(a) - y1*Math.sin(a);
				point[7] = y1*Math.cos(a) + x1*Math.sin(a);
			}
		}
		if (transformType == "rotation") {
			if (x != 0) points = zim.transformPoints(points, "x", xStart);
			if (y != 0) points = zim.transformPoints(points, "y", yStart);
		}
		return points;
	}//-27.95

/*--
zim.makeID = function(length, type, letterCase)

makeID
zim function

DESCRIPTION
makes a random letter, number or mixed id of specified length

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var id1 = makeID(); // five random letters and numbers (starts with letter)
var id2 = makeID(null, "string"); // five random uppercase letters
var id3 = makeID(10, "number"); // ten random numbers
var id4 = makeID(5, ["Z", "I", "M", 1, 2, 3, 4, 5, "-"]); // random five characters from array (possibly repeating)
END EXAMPLE

PARAMETERS
length - (default 5) the length of the id
type - (default "mixed") set to "letters" or "numbers" as well
	note: no O, 0, 1, I or L due to identification problems
	pass in an array of characters to make an id from only those characters
letterCase - (default uppercase) - set to "lowercase" or "mixed" as well

RETURNS a String id (even if type is number)
--*///+13.5
	zim.makeID = function(type, length, letterCase) {
		z_d("13.5");
		if (zot(type)) type = "mixed";
		if (zot(length)) length = 5;
		if (zot(letterCase)) letterCase = "uppercase";
		var choices;
		var nums = [2,3,4,5,6,7,8,9];
		var lets = "abcdefghjkmnpqrstuvwxyz".split("");
		if (type.constructor === Array) {
			choices = type;
		} else if (type == "numbers") {
			choices = nums;
		} else if (type == "letters") {
			choices = lets;
		} else {
			choices = nums.concat(lets);
		}
		var id = "";
		var c; // character - note, char is a reserved word for compressor!
		var rand;
		for (var i=0; i<length; i++) {
			c = choices[Math.floor(Math.random()*choices.length)];
			rand = Math.random();
			if (letterCase == "uppercase" || (letterCase == "mixed" && rand > .5)) {
				if (c.toUpperCase) c = c.toUpperCase();
			} else {
				if (c.toLowerCase) c = c.toLowerCase();
			}
			id += String(c);
		}
		return id;
	}//-13.5

/*--
zim.swapProperties = function(property, objA, objB)

swapProperties
zim function

DESCRIPTION
Pass in a property as a string and two object references
and this function will swap the property values.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// exchanges the x position of two ZIM circles
swapProperties("x", circle1, circle2); stage.update();
END EXAMPLE

PARAMETERS
property - a String of the property to swap values eg. "alpha"
objA, objB - the objects on which to swap properties

RETURNS Boolean indicating success
--*///+17.1
	zim.swapProperties = function(property, objA, objB) {
		z_d("17.1");
		if (zot(objA) || zot(objB) || zot(objA[property]) || zot(objB[property])) return false;
		var temp = objB[property];
		objB[property] = objA[property];
		objA[property] = temp;
		return true;
	}//-17.1

/*--
zim.mobile = function(orientation)

mobile
zim function

DESCRIPTION
Detects if app is on a mobile device - if so, returns the mobile device type:
android, ios, blackberry, windows, other (all which evaluate to true) else returns false.
orientation defaults to true and if there is window.orientation then it assumes mobile
BUT this may return true for some desktop and laptop touch screens
so you can turn the orientation check off by setting orientation to false.
If orientation is set to false the check may miss non-mainstream devices
The check looks at the navigator.userAgent for the following regular expression:
/ip(hone|od|ad)|android|blackberry|nokia|opera mini|mobile|phone|nexus|webos/i
Microsoft mobile gets detected by nokia, mobile or phone.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
if (mobile()) {
	var pane = new Pane(300, 200, "Desktop Only");
	pane.show();
}
END EXAMPLE

PARAMETERS
orientation - (default true) uses window.orientation property to determine mobile
	this may call certain touch screens mobile
	but setting to false uses a test on mobile names which could be incomplete

RETURNS a String or false
--*///+28
	zim.mobile = function(orientation) {
		z_d("28");
		if (zot(orientation)) orientation = true;
		if (/ip(hone|od|ad)/i.test(navigator.userAgent)) return "ios";
		if (/android|nexus/i.test(navigator.userAgent)) return "android";
		if (/blackberry/i.test(navigator.userAgent)) return "blackberry";
		if (/nokia|phone|mobile/i.test(navigator.userAgent)) return "windows";
		if (/opera mini|webos/i.test(navigator.userAgent)) return "other";
		if (orientation && window.orientation !== undefined) return true;
		return false;
	}//-28

//
/*--
zim.vee = function(obj)

vee
zim function

DESCRIPTION
Determines if obj is a ZIM Pick() object or a Pick Literal - used by ZIM VEE parameters
Pick Literal format is [], {min:a, max:b}, function(){}, {noPick:x} or a function(){}
See https://zimjs.com/docs.html?type=Pick
ZIM Pick is a way to pass in dynamic parameters or style properties
This is very handy to pass in a series() function or an array for random pickings, etc.
Used to create dynamic particles with the Emitter or tile specific items in order, etc.
Pick.choose() accepts any value and if not in ZIM Pick format, will just return the object

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var color = [red, green, blue];
// ternary operator - if in Pick format, add "random" else ""
new Label((vee(color)?"random ":"") + "colors").center();
END EXAMPLE

PARAMETERS
orientation - (default true) uses window.orientation property to determine mobile
	this may call certain touch screens mobile
	but setting to false uses a test on mobile names which could be incomplete

RETURNS a Boolean true if Pick format or false if not (such as just a number, string, new Circle, etc.)
--*///+28.5
	zim.vee = function(obj) {
		z_d("28.5");
		return !zot(obj) && (obj.type == "Pick" || Array.isArray(obj) || (obj.constructor == {}.constructor && (!zot(obj.max) || !zot(obj.noPick))) || typeof obj == "function");
	}//-28.5

/*--
zim.extend = function(subclass, superclass, override, prefix, prototype)

extend
zim function - modified CreateJS extend and promote utility methods

DESCRIPTION
For ES5 - place after a sub class to extend a super class.
Extending a super class means that the sub class receives all the properties and methods of the super class.
For example, a ZIM Container() extends a CreateJS Container and then adds more methods and properties
but all the CreateJS Container methods and properties are still there too like x, y, addChild(), etc.

For ES6 - do not use zim.extend() but rather use the built in ES6 structures as follows:

EXAMPLE
// ES6 - do NOT use zim.extend()
class Person() {
	constructor () {
		zog("I am a person");
	}
}
class Woman extends Person { // use JS6 extends keyword
	constructor () {
		super(); // use JS6 super() to call the Person constructor - will do the zog()
		// Woman code
	}
}

// ES6 to extend a zim Container for example (do NOT use zim.extend() in ES6)
class ChineseCoin extends Container { // use JS6 extends keyword
	constructor () {
		super(); // must call the zim Container before using keyword this
		new Circle(100, "gold").addTo(this); // this will be the zim Container
		new Rectangle(100, 100, "brown").center(this);
	}
}
var coin = new ChineseCoin().center(stage); // coin is a zim Container with Circle and Rectangle inside
END EXAMPLE

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// ES5 examples using functions to make classes
// ES5 has no extends keyword and no super keyword so we use zim.extends()
function Person() {
	this.talk = function() {
		zog("I am a person");
	}
}
function Woman() {
	this.super_constructor(); // part of the zim.extend() system
}
extend(Woman, Person); // here is the zim.extend() for ES5
var woman = new Woman();
woman.talk();
END EXAMPLE

NOTE: CreateJS display objects require their constructor to be called otherwise it is like quantum entanglement (seriously)
extend() adds access to the super class constructor so it can be called in the subclass as follows:
this.super_constructor();
It also provides access to super class methods that are overridden

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// ES5 example - see the ES6 set of examples for ES6 ;-)
// make a Collection class that will extend a Container
// the Collection class will call the Container constructor
// and override the the ZIM Container center method in the class body
// and override the CreateJS Container addChild method in the prototype
// either method would work in either place - it is often a matter of preference
// but you might need to use a method in the class body to access local variables
// The ZIM extend() method parameter values need to change depending on where you override
// see the comments inline for the instructions

var Collection = function() {
	// for CreateJS the super constructor must be run
	this.super_constructor();

	// override the zim center() method
	// methods in the function call that override must be passed in as an array of strings
	// to the override parameter of extend() to be able to access the super_method
	this.center = function(where) {
		this.super_center(where);
		this.y -= 50;
	}
}
// override the super class addChild() that comes from the CreateJS Container
// methods on the prototype that override are automatically provided a super_method
// unless the prototype parameter of extend() is set to false (default is true)
Collection.prototype.addChild = function(c) {
	this.super_addChild(c); // call the super class addChild
	zog("added a child to Collection");
}

// make the Collection extend a Container()
// it will receive all the properties and methods of the Container plus its own
extend(Collection, Container, "center"); // or pass an array of overridden methods

// use the Collection
var c = new Collection();
c.addChild(new Rectangle(100, 100, green)); // zogs "added a child to Collection"
c.center(stage); // centers the collection but then offsets it 50 pixels up
END EXAMPLE

NOTE: the superclass constructor is always available as this.prefix_constructor() no matter the override or prototype settings
NOTE: this.prefix_constructor(); should be called at the top of the subclass to avoid problems when multiple copies of object
NOTE: to extend a class that already extends a ZIM class then change the prefix to a unique name:

EXAMPLE
// if we already had the Collection example above and we want to extend that
// then we must use a new prefix when using extend()

var Records = function() {
	this.Collection_constructor();
}
extend(Records, Collection, null, "Collection");

// you will still have this.super_center(), this.super_addChild() if needed
// plus any newly overridden methods available as this.Collection_methodName() etc.
var r = new Records();
r.addChild(new Circle(20, pink));
r.super_center(stage); // call the original center (without vertical shift)

// to extend again, use yet another prefix - for example: "Records"
var Jazz = function() {
	this.Records_constructor();
}
extend(Jazz, Records, null, "Records");
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
NOTE: do NOT use zim.extend() with ES6 - see ES6 examples at top instead
subclass - the class to extend
superclass - the class to extend from (an existing class)
override - (default null) an Array of methods (as Strings) to override.
	You can override any function by just defining that function in the subclass
	the override parameter gives you access to the overridden function in the superclass prototype
	only methods on the superclass prototype can be accessed once overridden - not methods in the superclass body
	if there is only one method being overridden then a single string is fine ("test" or ["test"] is fine)
	any methods passed to this parameter will be given prefix_methodName() access on the sub class (this.prefix_methodName())
	where the prefix is below (note, the prototype setting has no bearing on these manual overrides)
	this list is only needed for methods in the subclass body
	methods assigned to the prototype of the subclass that override are automatically given prefixes
prefix - (default "super") a prefix that will be followed by "_" and then the overridden method name
	by default this.super_constructor() would call the super class constructor
	if prefix is set to "Person" then this.Person_constructor() would call the super class constructor
	the same system is used to call overridden files in override or prototype
prototype - (default true) will search the subclass prototype for overriding methods
	the overridden methods are then available as this.prefix_methodName()
	set to false to avoid searching the super class for methods overridden by the sub class prototype
	just quickens the code minutely if there is no need

NOTE: extend() is included in Distill if DISPLAY, METHODS or FRAME Module classes are used (otherwise NOT included)

RETURNS the subclass
--*///+50.35
	zim.extend = function(subclass, superclass, override, prefix, prototype) {
		var sig = "subclass, superclass, override, prefix, prototype";
		var duo; if (duo = zob(zim.extend, arguments, sig)) return duo;
		if (zot(subclass) || zot(superclass)) {
			if (zon && subclass!=zim.StageGL) zog("zim.extend() - please supply a class and its superclass");
			return;
		}
		if (zot(prefix)) prefix = "super";
		if (zot(override)) override = [];
		if (!Array.isArray(override)) override = [override];
		if (zot(prototype)) prototype = true;
		// modified CreateJS extend() to include any prototype members already added
		// see http://www.createjs.com/docs/easeljs/classes/Utility%20Methods.html
		var existingP = {};
		for (var f in subclass.prototype) Object.defineProperty(existingP,f,Object.getOwnPropertyDescriptor(subclass.prototype, f));
		function o() {this.constructor = subclass;}
		o.prototype = superclass.prototype;
		subclass.prototype = new o();
		for (f in existingP) Object.defineProperty(subclass.prototype,f,Object.getOwnPropertyDescriptor(existingP,f));

		// modified CreateJS promote() to promote methods other than constructor only if methods is true
		// zim does not override with prototypes so it is uneccessary to loop through the super class methods
		// added checking an array of string values of methods defined in class (not prototype) that are being overridden
		var subP = subclass.prototype;
		var supP = (Object.getPrototypeOf&&Object.getPrototypeOf(subP))||subP.__proto__;
		if (supP) {
			subP[(prefix+="_") + "constructor"] = supP.constructor; // constructor is not always innumerable
			var n;
			for (var i=0; i<override.length; i++) {
				n = override[i];
				if (typeof supP[n] == "function") {subP[prefix + n] = supP[n];}
			}
			if (prototype) {
				for (n in supP) {
					if (subP.hasOwnProperty(n) && (typeof supP[n] == "function")) {subP[prefix + n] = supP[n];}
				}
			}
		}
		return subclass;
	}
	//-50.35

// SUBSECTION BASICS

/*--
zim.copy = function(obj, clone, cloneContainers)

copy
zim function

DESCRIPTION
Copies arrays and basic objects:
modified http://stackoverflow.com/users/35881/a-levy
If you have var obj = {prop:"val"};
and then try and copy obj to obj2 like so: obj2 = obj;
then obj2 and obj refer to the same object.
This means that after obj.prop = "new"; both obj.prop and obj2.prop would be "new".
copy(obj) returns a new object so both will work independently
and after obj.prop = "new"; obj2.prop would still be "val".

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var obj = {hair:blue, cars:["audi", "honda"]};
var cop = copy(obj);
cop.hair = "green";
zog(obj.hair, obj.cop); // blue, green
obj.cars.push("vw");
zog(obj.cars.length, cop.cars.length); // 3, 2

// copy with clone for cloneable objects
// without the second parameter as true these obj[0] and obj2[0] would be the same
// and when we do the second addTo it would just move the circle to the second position
var obj = [
	new Circle(20,green),
	new Rectangle(30,30,green),
	new Triangle(40,40,40,green)
];
var obj2 = copy(obj, true); // copy and clone
obj[0].addTo(stage).pos(100, 200);
obj2[0].addTo(stage).pos(300, 400);
END EXAMPLE

PARAMETERS
obj - the object to copy
clone - (default false) set to true to clone any cloneable object while copying
cloneContainers - (default true if clone true) set to false to not copy objects with type="Container"

RETURNS a new Object
--*///+10
	zim.copyCheck = false;
	zim.copy = function(obj, clone, cloneContainer) {
		if (!zim.copyCheck) {z_d("10"); zim.copyCheck = true;}
		if (zot(clone)) clone = false;
		if (zot(cloneContainer)) cloneContainer = true;
		if (obj==null || !(obj instanceof Array || obj.constructor == {}.constructor)) return clone&&obj!=null?(obj.clone?(obj.type&&((obj.type!="Container"&&obj.type!="Stage"&&obj.type!="StageGL")||cloneContainer)?obj.clone():obj):obj):obj;
		if (obj instanceof Array) {
			var array = [];
			for (var i=0; i<obj.length; i++) {
				array[i] = zim.copy(obj[i], clone, cloneContainer);
			}
			return array;
		}
		if (obj.constructor == {}.constructor) {
			var copy = {};
			for (var attr in obj) {
				var answer = zim.copy(obj[attr], clone, cloneContainer);
				if (obj.hasOwnProperty(attr)) copy[attr] = answer;
			}
			return copy;
		}
	}//-10

/*--
zim.merge = function(objects)

merge
zim function

DESCRIPTION
Merges any number of objects {} you pass in as parameters.
Overwrites properties if they have the same name.
Returns a merged object with original objects kept intact.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var one = {food:"chocolate"};
var two = {drink:"milk"};
var tri = merge(one, two);
zog(tri.food, tri.drink); // chocolate, milk
END EXAMPLE

PARAMETERS
objects - a list of objects (any number) to merge together

RETURNS a new Object
--*///+12
	zim.mergeCheck = false;
	zim.merge = function() {
		if (!zim.mergeCheck) {z_d("12"); zim.mergeCheck = true;}
		var obj = {}; var i; var j;
		for (i=0; i<arguments.length; i++) {
			for (j in arguments[i]) {
				if (arguments[i].hasOwnProperty(j)) {
					obj[j] = arguments[i][j];
				}
			}
		}
		return obj;
	}//-12

/*--
zim.arraysEqual = function(a, b, strict)

arraysEqual
zim function

DESCRIPTION
Finds out if arrays are same (including nested arrays).
Works for arrays with strings and numbers (not necessarily other objects).
(Slightly modified Evan Steinkerchnerv & Tomas Zato)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var one = [1,2,"wow",[3,4]];
var two = [1,2,"wow",[3,4]];
zog(arraysEqual(one, two)); // true
one[3][1] = 5;
zog(arraysEqual(one, two)); // false
END EXAMPLE

PARAMETERS
a, b - the arrays to check to see if they are equal
strict - (default true) set to false so order in arrays does not matter

RETURNS a Boolean
--*///+11
	zim.arraysEqual = function(a, b, strict) {
		z_d("11");
		if (zot(a) || zot(b)) return false;
		if (zot(strict)) strict = true; // must be the same order
		if (a.length != b.length) return false;

		for (var i = 0; i < a.length; i++) {
			if (a[i] instanceof Array && b[i] instanceof Array) {
				if (!zim.arraysEqual(a[i], b[i], strict))	return false;
			}
			else if (strict && a[i] != b[i]) {
				return false;
			}
			else if (!strict) {
				return zim.arraysEqual(a.sort(), b.sort(), true);
			}
		}
		return true;
	}//-11

/*--
zim.isEmpty = function(obj)

isEmpty
zim function

DESCRIPTION
returns whether an object literal is empty

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var o = {};
zog( isEmpty(o) ); // true
o.test = 9;
zog( isEmpty(o) ); // false
END EXAMPLE

PARAMETERS
obj - the object literal to test

RETURNS a Boolean
--*///+11.5
	zim.isEmpty = function(obj) {
		z_d("11.5");
		if (zot(obj)) return;
		var count = 0;
		for (var o in obj) {
			count++; break;
		}
		return (count == 0);
	}//-11.5

/*--
zim.isJSON = function(str)

isJSON
zim function

DESCRIPTION
returns whether a string is a JSON string

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var s = '{"age":7,"name":"Dan Zen"}';
zog( isJSON(s) ); // true
var b = "hello";
zog( isJSON(b) ); // false
END EXAMPLE

PARAMETERS
str - the string to test

RETURNS a Boolean
--*///+11.6
	zim.isJSON = function(str) {
		z_d("11.6");
		try {
			return (JSON.parse(str).constructor == {}.constructor);
		} catch (e) {
			return false;
		}
	}//-11.6


/*--
zim.decimals = function(num, places, addZeros, addZerosBefore, includeZero, time)

decimals
zim function

DESCRIPTION
Rounds number to the number of decimal places specified by places.
Negative number places round to tens, hundreds, etc.
If addZeros is set to a number it adds 0 in empty spaces up to that many places after the decimal
If addZerosBefore is set to a number it adds 0 in empty spaces up to that many places before the decimal

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var score = 1.234;
score = decimals(score);
zog(score); // 1.2
zog(decimals(1.8345, 2)); // 1.83
zog(decimals(123,-1)); // 120
zog(decimals(2.3,null,2)); // 2.30
zog(decimals(3,null,null,2)); // 03
zog(decimals(.12,2,2,1,null,true)); // 0:12
END EXAMPLE

PARAMETERS
num - the Number to operate on
places - (default 1) how many decimals to include (negative for left of decimal place)
addZeros - (default 0) set to number of places to fill in zeros after decimal (and return String)
addZerosBefore - (default 0) set to number of places to fill in zeros before decimal (and return String)
includeZero - (default true) set to false to always have zero just be 0 without any extra zeros
time - (default false) a swap of : for . to handle minutes and seconds (not hours)

RETURNS a rounded Number or a String if addZeros, addZerosBefore or time is true
--*///+13
	zim.zut = function(e) {
		if (zot(e) || typeof e == "object") return true;
	}
	zim.zimDecimalCheck = false;
	zim.decimals = function(num, places, addZeros, addZerosBefore, includeZero, time, evt) {
		if (!zim.zimDecimalCheck) z_d("13");
		zim.zimDecimalCheck = true;
		if (zot(num)) return 0;
		if (zot(places)) places = 1;
		if (zot(addZeros)) addZeros = 0;
		if (zot(addZerosBefore)) addZerosBefore = 0;
		if (zot(addZerosBefore)) addZerosBefore = 0;
		if (zot(includeZero)) includeZero = true;
		if (zot(time)) time = false;
		// if (addZeros && places < 0) {
		// 	var place = String(num).indexOf(".");
		// 	var length = String(num).length;
		// 	var left = (place < 0) ? length : place;
		// 	for (var i=0; i<-places-left; i++) {num = "0" + num;}
		// 	return num;
		// }
		var answer = Math.round(num*Math.pow(10, places))/Math.pow(10, places);
		if (time) {
			var secs = answer - Math.floor(answer);
			answer = zim.decimals(Math.floor(answer) + secs*60/100, 2);
		}

		// if (addZeros && places > 0 && answer != 0) {
		// 	var place = String(answer).indexOf(".");
		// 	var length = String(answer).length;
		// 	if (place < 0) {place = length++; answer+=".";}
		// 	for (var i=0; i<places-(length-place-1); i++) {answer += "0";}
		// }
		var sign = zim.sign(answer);
		if (addZeros > 0) {
			var place = String(answer).indexOf(".");
			var length = String(answer).length;
			if (place < 0) {place = length++; answer+=".";}
			for (var i=0; i<addZeros-(length-place-1); i++) {answer += "0";}
		}
		if (addZerosBefore > 0) {
			if (sign == -1) answer = answer.substr(1,answer.length-1);
			var place = String(answer).indexOf(".");
			var length = String(answer).length;
			var left = (place < 0) ? length : place;
			for (var i=0; i<addZerosBefore-left; i++) {answer = "0" + answer;}
			if (sign == -1) answer = "-" + answer;
		}
		if ((addZeros + addZerosBefore > 0) && !includeZero && Number(answer) == 0) answer = 0;
		if (time) answer = String(answer).replace(".", ":");
		return zim.zut(evt) ? answer : null;
	}//-13

/*--
zim.sign = function(num)

sign
zim function

DESCRIPTION
returns -1, 0 or 1 depending on whether the number is less than, equal to or greater than 0

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var speed = 20;
zog(sign(speed)); // 1

var speed = 0;
zog(sign(speed)); // 0

var speed = -20;
zog(sSign(speed)); // -1
END EXAMPLE

PARAMETERS
num - the Number to operate on

RETURNS -1, 0 or 1
--*///+13.1
	zim.zimSignCheck = false;
	zim.sign = function(num) {
		if (!zim.zimSignCheck) z_d("13.1");
		zim.zimSignCheck = true;
		return num?num<0?-1:1:0;
	}//-13.1


/*--
zim.constrain = function(num, min, max, negative)

constrain
zim function

DESCRIPTION
returns a number constrained to min and max

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var cirle.x = constrain(circle.x, circle.radius, stageW-circle.radius);
// circle.x will not be smaller than the radius or bigger than stageW-radius

var speed = constrain(speed, minSpeed, maxSpeed, true);
// will confine the speed between minSpeed and maxSpeed if speed is positive
// and confine the speed between -maxSpeed and -minSpeed if the speed is negative
END EXAMPLE

PARAMETERS
num - the number to be constrained
min - (default 0) the minimum value of the return number
max - (default Number.MAX_VALUE) the maximum value of the return number
negative - (default false) allow the negative range of min and max when num is negative

RETURNS num if between min and max otherwise returns min if less or max if greater (inclusive)
RETURNS num between -max and -min if num is negative and negative parameter is set to true
--*///+13.2
	zim.constrain = function(num, min, max, negative) {
		z_d("13.2");
		if (zot(num)) return;
		if (zot(min)) min = 0;
		if (zot(max)) max = Number.MAX_VALUE;
		if (max < min) {max2 = min; max = min; min = max2;} // ES6 Fix to come
		if (zot(negative)) negative = false;
		if (negative && num < 0) {
			return Math.max(-max, Math.min(num, -min));
		} else {
			return Math.max(min, Math.min(num, max));
		}
	}//-13.2

/*--
zim.dist = function(a, b, c, d)

dist
zim function

DESCRIPTION
Calculates the distance between two points.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// using point values for each
var p1 = new Point(100, 100); // or {x:100, y:100}
var p2 = new Point(200, 200);
zog(dist(p1, p2)); // 141.42...
END EXAMPLE

EXAMPLE
// using x and y values for each
// do not rely on stage.mouseX, stage.mouseY - they do not work on touchscreen!
var stageX;
var stageY;
stage.on("stagemousemove", function (e) {stageX = e.stageX; stageY = e.stageY});
var distance = dist(stageW/2, stageH/2, stageX, stageY);
// distance of mouse from center of stage
END EXAMPLE

PARAMETERS
a - first Point - any object with x and y values - eg. a zim Container or zim Point or {x:10, y:30}
	or if four parameter values, an x value of the first point
b - second Point - any object with x and y values
	or if four parameter values, a y value of the first point
c - (default null) an x value of a second point - if using x and y values
d - (default null) a y value of a second point - if using x and y values

RETURNS a positive Number that is the distance (could be on an angle)
--*///+13.3
	zim.zimDistCheck = false;
	zim.dist = function(a, b, c, d) {
		if (!zim.zimDistCheck) z_d("13.3");
		zim.zimDistCheck = true;
		if (zot(a) || zot(b)) return;
		if (!zot(a.x) && !zot(b.x)) {
			d = b.y;
			c = b.x;
			b = a.y;
			a = a.x;
		} else {
			if (zot(c)) c = 0;
			if (zot(d)) d = 0;
		}
		return Math.sqrt((Math.pow(c-a, 2) + Math.pow(d-b, 2)));
	}//-13.3

//
/*--
zim.rectIntersect = function(a, b, margin)

rectIntersect
zim function

DESCRIPTION
Returns true if two rectangles are intersecting - this is a very fast but exact calculation

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// using point values for each
var r1 = {x:100, y:100, width:300, height:200}
var r2 = new Boundary(50,200,100,100);
zog(rectIntersect(r1, r2)); // true
END EXAMPLE

PARAMETERS
a - first rectangle with x, y, width and height properties - such as a bounds or Boundary
	make sure that these are in the same coordinate systems - use ZIM boundsToGlobal for instance
b - second rectangle with x, y, width and height properties
margin - (default 0) positive value adds margin (more likely to intersect) and negative subtracts (less likely to intersect)

RETURNS a Boolean as to whether rectangles are intersecting
--*///+13.32
	zim.zimRectIntersectCheck = false;
	zim.rectIntersect = function(a, b, margin) {
		if (!zim.zimRectIntersectCheck) z_d("13.32");
		zim.zimRectIntersectCheck = true;
		if (zot(margin)) margin = 0;
		if (a.x >= b.x + b.width + margin || a.x + a.width + margin <= b.x ||
			a.y >= b.y + b.height + margin || a.y + a.height + margin <= b.y ) {
			return false;
		} else {
			return true;
		}
	}//-13.32

//
/*--
zim.boundsAroundPoints = function(points)

boundsAroundPoints
zim function

DESCRIPTION
Returns a rectangle {x,y,width,height} around an array of points {x,y}

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var blob = new Blob();
var points = blob.interpolate();
zog(boundsAroundPoints(points)); // {x:-100, y:-100, width:200, height:200}
// could call this after resizing the blob to get the rough bounds of the blob (or squiggle)
// interpolate defaults to 1 so setting 5 would be even more precise, etc.
END EXAMPLE

PARAMETERS
points - an array of points with x and y properties.

RETURNS an object with x, y, width and height properties
representing the rectangle around the points provided
--*///+13.34
	zim.zimBoundsAroundPointsCheck = false;
	zim.boundsAroundPoints = function(points) {
		if (!zim.zimBoundsAroundPointsCheck) z_d("13.34");
		zim.zimBoundsAroundPointsCheck = true;
		var tX = 10000;
		var tY = 10000;
		var bX = -10000;
		var bY = -10000;
		for (var i=0; i<points.length; i++) {
			var p = points[i];
			if (p.x < tX) tX = p.x;
			if (p.x > bX) bX = p.x;
			if (p.y < tY) tY = p.y;
			if (p.y > bY) bY = p.y;
		}
		return {x:tX, y:tY, width:bX-tX, height:bY-tY};
	}//-13.34

/*--
zim.angle = function(x1, y1, x2, y2)

angle
zim function

DESCRIPTION
Calculates the angle between two points relative to the positive x axis

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var angle = angle(stageW/2, stageH/2, stageW/2+100, stageH/2+100); // 45
// angle from center of stage to 100, 100 to the right and down from the center of the stage

var angle2 = angle(stageW/2, stageH/2, stageW/2-100, stageH/2+100); // 135

var angle3 = angle(stageW/2, stageH/2, stageW/2+100, stageH/2-100); // 315
END EXAMPLE

PARAMETERS
x1, y1 - first point x and y
	unless no second point in which case x1, y1 will be second point and first point will be 0, 0
x2, y2 - second point x and y

RETURNS a positive Number that is the angle between first and second point relative to positive x axis
--*///+13.4
	zim.angle = function(x1, y1, x2, y2) {
		z_d("13.4");
		if (zot(x1) || zot(y1)) return;
		if (zot(x2)) {x2 = x1; x1 = 0};
		if (zot(y2)) {y2 = y1; y1 = 0};
		return (Math.atan2(y2-y1, x2-x1)*180/Math.PI+360)%360;
	}//-13.4

/*--
zim.smoothStep = function(num, min, max)

smoothStep
zim function

DESCRIPTION
smoothStep takes an input value and outputs a value between 0 and 1
that represents a transition between the min and max with easing at both ends.
If you want the easing to be more pronounced, then reduce difference between min and max.
If the value falls outside the min or max then it is set to the min or max.
Remember the return value is between 0 and 1 so you can multiply by max-min and add it to min
to get a value at the original scale.
Used to make blobs with Noise(): https://zimjs.com/noise/blobs.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// here we use smoothStep to make a gradient between black and white
// not an even one right across but a gradient across a transition zone of 40-100

// create an empty Bitmap size 200, 200 and center it on the stage
var bmp = new Bitmap(null, 200, 200).center(stage);

// we need to loop and get a value for each pixel
// normally we loop across the rows and then do each column
// but here we are making a horizontal gradient
// so we will loop across the x and get the desired value
// then when we loop across the y in the inner loop, we just use that same value
for (var x = 0; x < bmp.width; x++) {
	// making gradient in x only so calculate smoothStep here
	// x will be from 0 to the width of 200
	// we pass in a min of 40 and a max of 100
	// the result of smoothStep is between 0 and 1
	// so from 0 to 40 the return of smoothStep will be 0
	// and from 100 to 200 the return of smoothStep will be 1
	// In between, the return value starts off close to 0, then speeds up
	// and then slows down to 1 in a curve that is somewhat like the letter f
	// When we multiply by 255 and apply that result to each color,
	// we get black and then a range of greys and then white
	var value = smoothStep(x, 40, 100)*255;

	// now we loop down the column for the x position
	for (var y = 0; y < bmp.height; y++) {
		// imageData is four values per pixel
		// the red, green, blue and alpha
		// in one big long array - each value will be constrained to between 0 and 255
		// this i value will increase by 4 each time
		// then we write the same value for red, green, blue to get a shade of grey
		var i = (x + y * bmp.width) * 4;
		bmp.imageData.data[i] = value; // red (0-255)
		bmp.imageData.data[i + 1] = value; // green (0-255)
		bmp.imageData.data[i + 2] = value; // blue (0-255)
		bmp.imageData.data[i + 3] = 255; // alpha (0-255)
	}
}
bmp.drawImageData(); // draw the imageData to the Bitmap
END EXAMPLE

PARAMETERS
num - the input value with respect to min and max
min - the lower edge for smoothStep (often termed edge0) - anything smaller will be set to min
max - the upper edge for smoothStep (often termed edge1) - anything bigger will be set to max

RETURNS a number between 0 and 1 that represents a transition factor
--*///+13.7
	zim.smoothStep = function(num, min, max) {
		z_d("13.7");
	    var x = zim.constrain((num - min)/(max - min), 0, 1);
	    return x*x*x*(x*(x*6 - 15) + 10); // Perlin
	}//-13.7

// SUBSECTION CLASSES

/*--
zim.Noise = function(seed)

Noise
zim class

DESCRIPTION
Noise creates OpenSimplex Noise: https://en.wikipedia.org/wiki/OpenSimplex_noise
Converted from https://www.npmjs.com/package/open-simplex-noise
See examples at https://zimjs.com/noise/
In general, this is special noise where the pixels relate to one another in a complex way.
This connection, lets us do things like create terrains or blobs, etc. that look organic.
There is 1D, 2D, 3D, and 4D noise where we pass in one value, two values, three values and four values.
We always get back a number between -1 and 1 and this result relates to the results around it.

1D - we can plot 1D by drawing line segments across the stage (x) and setting the y value to the result of simplex1D(x)
This makes a 2D mountain-like terrain across the stage

2D - if we keep the plot from the 1D but use 2D and change the second parameter, we can animate the line.
We just need to adjust the second parameter by a very small amount each time such as .005.
Or we can plot put the return value of simplex2D onto its x,y matching location in a Bitmap
mapping it to a greyscale to make a traditional noise pattern.
We can adjust the "size" of the noise by dividing the x and y values (frequency).
If we use the ZIM smoothStep() function we can smoothen these to make blobs.
We can also use the return value as height for 3D terrain.

3D - if we keep the traditional noise/blob pattern from the 2D but use simplex3D and animate the third parameter,
we can animate the 2D noise in time which looks great when we animate blobs!
This plotting is thousands of computations and will bog the computer if too big.

4D - will allow us to animate 3D values, etc.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// 1D Noise to make a jagged line across the stage
var noise = new Noise();
var shape = new Shape(stageW, stageH).addTo(stage);
shape.graphics.s("black").ss(2).mt(0, stageH/2);
loop(stageW/50, function(i) {
	shape.graphics.lt((i+1)*50, stageH/2 + noise.simplex1D(i)*200);
});
// the above can be animated by using simplex2D and animating the second number by small amounts
END EXAMPLE

EXAMPLE
// 2D noise
// create a Noise object:
var noise = new Noise();

// create an empty Bitmap size 200, 200 into which to draw the noise
var bmp = new Bitmap(null, 200, 200).center(stage);

// we fill the bitmap starting from top left going across in the inner loop,
// then down, then across, etc. until we get to bottom right.
for (var y = 0; y < bmp.height; y++) {
	for (var x = 0; x < bmp.width; x++) {
		// the noise methods return a number from -1 to 1
		// by adding 1 we get a number between 0 and 2 and we divide by 2 to get 0-1
		// and we multiply this by 255 to get a number between 0 and 255
		var value = (noise.simplex2D(x,y)+1)/2 * 255;
		// imageData is one big array with four values per pixel
		// the red, green, blue and alpha
		// each value will constrained to between 0 and 255
		// the i value is how many on the current row plus the columns from the previous rows
		// and we set it to increase by 4 each time giving us a place for each color and alpha
		// We write the same value for red, green, blue to get a shade of grey
		var i = (x + y * bmp.width) * 4;
		bmp.imageData.data[i] = value; // red (0-255)
		bmp.imageData.data[i + 1] = value; // green (0-255)
		bmp.imageData.data[i + 2] = value; // blue (0-255)
		bmp.imageData.data[i + 3] = 255; // alpha (0-255)
	}
}
bmp.drawImageData(); // this draws the imageData to the Bitmap

// Here is the same example to get blobs using smoothStep:

var f = 25; // try changing this number around
for (var y = 0; y < bmp.height; y++) {
	for (var x = 0; x < bmp.width; x++) {
		var value = noise.simplex2D(x/f, y/f)+1)/2; // 0-1
		// smoothStep sets less than .3 to 0 and greater than .35 to 1
		// and transitions between using an easing formula in the shape of an f
		var value = smoothStep(value, .3, .35) * 255;
		var i = (x + y * bmp.width) * 4;
		bmp.imageData.data[i] = value; // red (0-255)
		bmp.imageData.data[i + 1] = value; // green (0-255)
		bmp.imageData.data[i + 2] = value; // blue (0-255)
		bmp.imageData.data[i + 3] = 255; // alpha (0-255)
	}
}
bmp.drawImageData();
END EXAMPLE

PARAMETERS
seed - (default Math.random()*1000000) keeping the same seed can remake a pattern the same

METHODS
simplex1D(x) - returns a noise value between -1 and 1
	In each method, the noise value relates to its neighbor rather than a completely random value
simplex2D(x,y) - returns a noise value between -1 and 1
simplex3D(x,y,z) - returns a noise value between -1 and 1
simplex4D(x,y,z,w) - returns a noise value between -1 and 1

PROPERTIES
seed - read only - the seed that was used for the Noise object
--*///+13.9
	zim.Noise = function(seed) {
		"use strict";
		z_d("13.9");

		if (zot(seed)) seed = Math.random()*1000000;
		var clientSeed = seed;
		this.seed = seed;

		var that = this;

		var con = {}; // holds the constants
		con.NORM_2D = 1.0 / 47.0;
		con.NORM_3D = 1.0 / 103.0;
		con.NORM_4D = 1.0 / 30.0;
		con.SQUISH_2D = (Math.sqrt(2 + 1) - 1) / 2;
		con.SQUISH_3D = (Math.sqrt(3 + 1) - 1) / 3;
		con.SQUISH_4D = (Math.sqrt(4 + 1) - 1) / 4;
		con.STRETCH_2D = (1 / Math.sqrt(2 + 1) - 1) / 2;
		con.STRETCH_3D = (1 / Math.sqrt(3 + 1) - 1) / 3;
		con.STRETCH_4D = (1 / Math.sqrt(4 + 1) - 1) / 4;
		con.base2D = [
			[1, 1, 0, 1, 0, 1, 0, 0, 0],
			[1, 1, 0, 1, 0, 1, 2, 1, 1]
		];
		con.base3D = [
			[0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1],
			[2, 1, 1, 0, 2, 1, 0, 1, 2, 0, 1, 1, 3, 1, 1, 1],
			[1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 2, 1, 1, 0, 2, 1, 0, 1, 2, 0, 1, 1]
		];
		con.base4D = [
			[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1],
			[3, 1, 1, 1, 0, 3, 1, 1, 0, 1, 3, 1, 0, 1, 1, 3, 0, 1, 1, 1, 4, 1, 1, 1, 1],
			[
				1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 2, 1, 1, 0, 0, 2, 1, 0, 1, 0, 2, 1, 0, 0, 1, 2, 0, 1, 1,
				0, 2, 0, 1, 0, 1, 2, 0, 0, 1, 1
			],
			[
				3, 1, 1, 1, 0, 3, 1, 1, 0, 1, 3, 1, 0, 1, 1, 3, 0, 1, 1, 1, 2, 1, 1, 0, 0, 2, 1, 0, 1, 0, 2, 1, 0, 0, 1, 2, 0, 1, 1,
				0, 2, 0, 1, 0, 1, 2, 0, 0, 1, 1
			]
		];
		con.gradients2D = [5, 2, 2, 5, -5, 2, -2, 5, 5, -2, 2, -5, -5, -2, -2, -5];
		con.gradients3D = [
			-11, 4, 4, -4, 11, 4, -4, 4, 11,
			11, 4, 4, 4, 11, 4, 4, 4, 11,
			-11, -4, 4, -4, -11, 4, -4, -4, 11,
			11, -4, 4, 4, -11, 4, 4, -4, 11,
			-11, 4, -4, -4, 11, -4, -4, 4, -11,
			11, 4, -4, 4, 11, -4, 4, 4, -11,
			-11, -4, -4, -4, -11, -4, -4, -4, -11,
			11, -4, -4, 4, -11, -4, 4, -4, -11
		];
		con.gradients4D = [
			3, 1, 1, 1, 1, 3, 1, 1, 1, 1, 3, 1, 1, 1, 1, 3,
			-3, 1, 1, 1, -1, 3, 1, 1, -1, 1, 3, 1, -1, 1, 1, 3,
			3, -1, 1, 1, 1, -3, 1, 1, 1, -1, 3, 1, 1, -1, 1, 3,
			-3, -1, 1, 1, -1, -3, 1, 1, -1, -1, 3, 1, -1, -1, 1, 3,
			3, 1, -1, 1, 1, 3, -1, 1, 1, 1, -3, 1, 1, 1, -1, 3,
			-3, 1, -1, 1, -1, 3, -1, 1, -1, 1, -3, 1, -1, 1, -1, 3,
			3, -1, -1, 1, 1, -3, -1, 1, 1, -1, -3, 1, 1, -1, -1, 3,
			-3, -1, -1, 1, -1, -3, -1, 1, -1, -1, -3, 1, -1, -1, -1, 3,
			3, 1, 1, -1, 1, 3, 1, -1, 1, 1, 3, -1, 1, 1, 1, -3,
			-3, 1, 1, -1, -1, 3, 1, -1, -1, 1, 3, -1, -1, 1, 1, -3,
			3, -1, 1, -1, 1, -3, 1, -1, 1, -1, 3, -1, 1, -1, 1, -3,
			-3, -1, 1, -1, -1, -3, 1, -1, -1, -1, 3, -1, -1, -1, 1, -3,
			3, 1, -1, -1, 1, 3, -1, -1, 1, 1, -3, -1, 1, 1, -1, -3,
			-3, 1, -1, -1, -1, 3, -1, -1, -1, 1, -3, -1, -1, 1, -1, -3,
			3, -1, -1, -1, 1, -3, -1, -1, 1, -1, -3, -1, 1, -1, -1, -3,
			-3, -1, -1, -1, -1, -3, -1, -1, -1, -1, -3, -1, -1, -1, -1, -3
		];
		con.lookupPairs2D = [0, 1, 1, 0, 4, 1, 17, 0, 20, 2, 21, 2, 22, 5, 23, 5, 26, 4, 39, 3, 42, 4, 43, 3];
		con.lookupPairs3D = [
			0, 2, 1, 1, 2, 2, 5, 1, 6, 0, 7, 0, 32, 2, 34, 2, 129, 1, 133, 1, 160, 5, 161, 5, 518, 0, 519, 0, 546, 4, 550, 4, 645,
			3, 647, 3, 672, 5, 673, 5, 674, 4, 677, 3, 678, 4, 679, 3, 680, 13, 681, 13, 682, 12, 685, 14, 686, 12, 687, 14, 712,
			20, 714, 18, 809, 21, 813, 23, 840, 20, 841, 21, 1198, 19, 1199, 22, 1226, 18, 1230, 19, 1325, 23, 1327, 22, 1352, 15,
			1353, 17, 1354, 15, 1357, 17, 1358, 16, 1359, 16, 1360, 11, 1361, 10, 1362, 11, 1365, 10, 1366, 9, 1367, 9, 1392, 11,
			1394, 11, 1489, 10, 1493, 10, 1520, 8, 1521, 8, 1878, 9, 1879, 9, 1906, 7, 1910, 7, 2005, 6, 2007, 6, 2032, 8, 2033,
			8, 2034, 7, 2037, 6, 2038, 7, 2039, 6
		];
		con.lookupPairs4D = [
			0, 3, 1, 2, 2, 3, 5, 2, 6, 1, 7, 1, 8, 3, 9, 2, 10, 3, 13, 2, 16, 3, 18, 3, 22, 1, 23, 1, 24, 3, 26, 3, 33, 2, 37, 2,
			38, 1, 39, 1, 41, 2, 45, 2, 54, 1, 55, 1, 56, 0, 57, 0, 58, 0, 59, 0, 60, 0, 61, 0, 62, 0, 63, 0, 256, 3, 258, 3, 264,
			3, 266, 3, 272, 3, 274, 3, 280, 3, 282, 3, 2049, 2, 2053, 2, 2057, 2, 2061, 2, 2081, 2, 2085, 2, 2089, 2, 2093, 2,
			2304, 9, 2305, 9, 2312, 9, 2313, 9, 16390, 1, 16391, 1, 16406, 1, 16407, 1, 16422, 1, 16423, 1, 16438, 1, 16439, 1,
			16642, 8, 16646, 8, 16658, 8, 16662, 8, 18437, 6, 18439, 6, 18469, 6, 18471, 6, 18688, 9, 18689, 9, 18690, 8, 18693,
			6, 18694, 8, 18695, 6, 18696, 9, 18697, 9, 18706, 8, 18710, 8, 18725, 6, 18727, 6, 131128, 0, 131129, 0, 131130, 0,
			131131, 0, 131132, 0, 131133, 0, 131134, 0, 131135, 0, 131352, 7, 131354, 7, 131384, 7, 131386, 7, 133161, 5, 133165,
			5, 133177, 5, 133181, 5, 133376, 9, 133377, 9, 133384, 9, 133385, 9, 133400, 7, 133402, 7, 133417, 5, 133421, 5,
			133432, 7, 133433, 5, 133434, 7, 133437, 5, 147510, 4, 147511, 4, 147518, 4, 147519, 4, 147714, 8, 147718, 8, 147730,
			8, 147734, 8, 147736, 7, 147738, 7, 147766, 4, 147767, 4, 147768, 7, 147770, 7, 147774, 4, 147775, 4, 149509, 6,
			149511, 6, 149541, 6, 149543, 6, 149545, 5, 149549, 5, 149558, 4, 149559, 4, 149561, 5, 149565, 5, 149566, 4, 149567,
			4, 149760, 9, 149761, 9, 149762, 8, 149765, 6, 149766, 8, 149767, 6, 149768, 9, 149769, 9, 149778, 8, 149782, 8,
			149784, 7, 149786, 7, 149797, 6, 149799, 6, 149801, 5, 149805, 5, 149814, 4, 149815, 4, 149816, 7, 149817, 5, 149818,
			7, 149821, 5, 149822, 4, 149823, 4, 149824, 37, 149825, 37, 149826, 36, 149829, 34, 149830, 36, 149831, 34, 149832,
			37, 149833, 37, 149842, 36, 149846, 36, 149848, 35, 149850, 35, 149861, 34, 149863, 34, 149865, 33, 149869, 33,
			149878, 32, 149879, 32, 149880, 35, 149881, 33, 149882, 35, 149885, 33, 149886, 32, 149887, 32, 150080, 49, 150082,
			48, 150088, 49, 150098, 48, 150104, 47, 150106, 47, 151873, 46, 151877, 45, 151881, 46, 151909, 45, 151913, 44,
			151917, 44, 152128, 49, 152129, 46, 152136, 49, 152137, 46, 166214, 43, 166215, 42, 166230, 43, 166247, 42, 166262,
			41, 166263, 41, 166466, 48, 166470, 43, 166482, 48, 166486, 43, 168261, 45, 168263, 42, 168293, 45, 168295, 42,
			168512, 31, 168513, 28, 168514, 31, 168517, 28, 168518, 25, 168519, 25, 280952, 40, 280953, 39, 280954, 40, 280957,
			39, 280958, 38, 280959, 38, 281176, 47, 281178, 47, 281208, 40, 281210, 40, 282985, 44, 282989, 44, 283001, 39,
			283005, 39, 283208, 30, 283209, 27, 283224, 30, 283241, 27, 283256, 22, 283257, 22, 297334, 41, 297335, 41, 297342,
			38, 297343, 38, 297554, 29, 297558, 24, 297562, 29, 297590, 24, 297594, 21, 297598, 21, 299365, 26, 299367, 23,
			299373, 26, 299383, 23, 299389, 20, 299391, 20, 299584, 31, 299585, 28, 299586, 31, 299589, 28, 299590, 25, 299591,
			25, 299592, 30, 299593, 27, 299602, 29, 299606, 24, 299608, 30, 299610, 29, 299621, 26, 299623, 23, 299625, 27,
			299629, 26, 299638, 24, 299639, 23, 299640, 22, 299641, 22, 299642, 21, 299645, 20, 299646, 21, 299647, 20, 299648,
			61, 299649, 60, 299650, 61, 299653, 60, 299654, 59, 299655, 59, 299656, 58, 299657, 57, 299666, 55, 299670, 54,
			299672, 58, 299674, 55, 299685, 52, 299687, 51, 299689, 57, 299693, 52, 299702, 54, 299703, 51, 299704, 56, 299705,
			56, 299706, 53, 299709, 50, 299710, 53, 299711, 50, 299904, 61, 299906, 61, 299912, 58, 299922, 55, 299928, 58,
			299930, 55, 301697, 60, 301701, 60, 301705, 57, 301733, 52, 301737, 57, 301741, 52, 301952, 79, 301953, 79, 301960,
			76, 301961, 76, 316038, 59, 316039, 59, 316054, 54, 316071, 51, 316086, 54, 316087, 51, 316290, 78, 316294, 78,
			316306, 73, 316310, 73, 318085, 77, 318087, 77, 318117, 70, 318119, 70, 318336, 79, 318337, 79, 318338, 78, 318341,
			77, 318342, 78, 318343, 77, 430776, 56, 430777, 56, 430778, 53, 430781, 50, 430782, 53, 430783, 50, 431000, 75,
			431002, 72, 431032, 75, 431034, 72, 432809, 74, 432813, 69, 432825, 74, 432829, 69, 433032, 76, 433033, 76, 433048,
			75, 433065, 74, 433080, 75, 433081, 74, 447158, 71, 447159, 68, 447166, 71, 447167, 68, 447378, 73, 447382, 73,
			447386, 72, 447414, 71, 447418, 72, 447422, 71, 449189, 70, 449191, 70, 449197, 69, 449207, 68, 449213, 69, 449215,
			68, 449408, 67, 449409, 67, 449410, 66, 449413, 64, 449414, 66, 449415, 64, 449416, 67, 449417, 67, 449426, 66,
			449430, 66, 449432, 65, 449434, 65, 449445, 64, 449447, 64, 449449, 63, 449453, 63, 449462, 62, 449463, 62, 449464,
			65, 449465, 63, 449466, 65, 449469, 63, 449470, 62, 449471, 62, 449472, 19, 449473, 19, 449474, 18, 449477, 16,
			449478, 18, 449479, 16, 449480, 19, 449481, 19, 449490, 18, 449494, 18, 449496, 17, 449498, 17, 449509, 16, 449511,
			16, 449513, 15, 449517, 15, 449526, 14, 449527, 14, 449528, 17, 449529, 15, 449530, 17, 449533, 15, 449534, 14,
			449535, 14, 449728, 19, 449729, 19, 449730, 18, 449734, 18, 449736, 19, 449737, 19, 449746, 18, 449750, 18, 449752,
			17, 449754, 17, 449784, 17, 449786, 17, 451520, 19, 451521, 19, 451525, 16, 451527, 16, 451528, 19, 451529, 19,
			451557, 16, 451559, 16, 451561, 15, 451565, 15, 451577, 15, 451581, 15, 451776, 19, 451777, 19, 451784, 19, 451785,
			19, 465858, 18, 465861, 16, 465862, 18, 465863, 16, 465874, 18, 465878, 18, 465893, 16, 465895, 16, 465910, 14,
			465911, 14, 465918, 14, 465919, 14, 466114, 18, 466118, 18, 466130, 18, 466134, 18, 467909, 16, 467911, 16, 467941,
			16, 467943, 16, 468160, 13, 468161, 13, 468162, 13, 468163, 13, 468164, 13, 468165, 13, 468166, 13, 468167, 13,
			580568, 17, 580570, 17, 580585, 15, 580589, 15, 580598, 14, 580599, 14, 580600, 17, 580601, 15, 580602, 17, 580605,
			15, 580606, 14, 580607, 14, 580824, 17, 580826, 17, 580856, 17, 580858, 17, 582633, 15, 582637, 15, 582649, 15,
			582653, 15, 582856, 12, 582857, 12, 582872, 12, 582873, 12, 582888, 12, 582889, 12, 582904, 12, 582905, 12, 596982,
			14, 596983, 14, 596990, 14, 596991, 14, 597202, 11, 597206, 11, 597210, 11, 597214, 11, 597234, 11, 597238, 11,
			597242, 11, 597246, 11, 599013, 10, 599015, 10, 599021, 10, 599023, 10, 599029, 10, 599031, 10, 599037, 10, 599039,
			10, 599232, 13, 599233, 13, 599234, 13, 599235, 13, 599236, 13, 599237, 13, 599238, 13, 599239, 13, 599240, 12,
			599241, 12, 599250, 11, 599254, 11, 599256, 12, 599257, 12, 599258, 11, 599262, 11, 599269, 10, 599271, 10, 599272,
			12, 599273, 12, 599277, 10, 599279, 10, 599282, 11, 599285, 10, 599286, 11, 599287, 10, 599288, 12, 599289, 12,
			599290, 11, 599293, 10, 599294, 11, 599295, 10
		];
		con.p2D = [0, 0, 1, -1, 0, 0, -1, 1, 0, 2, 1, 1, 1, 2, 2, 0, 1, 2, 0, 2, 1, 0, 0, 0];
		con.p3D = [
			0, 0, 1, -1, 0, 0, 1, 0, -1, 0, 0, -1, 1, 0, 0, 0, 1, -1, 0, 0, -1, 0, 1, 0, 0, -1, 1, 0, 2, 1, 1, 0, 1, 1, 1, -1, 0,
			2, 1, 0, 1, 1, 1, -1, 1, 0, 2, 0, 1, 1, 1, -1, 1, 1, 1, 3, 2, 1, 0, 3, 1, 2, 0, 1, 3, 2, 0, 1, 3, 1, 0, 2, 1, 3, 0, 2,
			1, 3, 0, 1, 2, 1, 1, 1, 0, 0, 2, 2, 0, 0, 1, 1, 0, 1, 0, 2, 0, 2, 0, 1, 1, 0, 0, 1, 2, 0, 0, 2, 2, 0, 0, 0, 0, 1, 1,
			-1, 1, 2, 0, 0, 0, 0, 1, -1, 1, 1, 2, 0, 0, 0, 0, 1, 1, 1, -1, 2, 3, 1, 1, 1, 2, 0, 0, 2, 2, 3, 1, 1, 1, 2, 2, 0, 0,
			2, 3, 1, 1, 1, 2, 0, 2, 0, 2, 1, 1, -1, 1, 2, 0, 0, 2, 2, 1, 1, -1, 1, 2, 2, 0, 0, 2, 1, -1, 1, 1, 2, 0, 0, 2, 2, 1,
			-1, 1, 1, 2, 0, 2, 0, 2, 1, 1, 1, -1, 2, 2, 0, 0, 2, 1, 1, 1, -1, 2, 0, 2, 0
		];
		con.p4D = [
			0, 0, 1, -1, 0, 0, 0, 1, 0, -1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 1, 0, 0, 0, 0, 1, -1, 0, 0, 0, 1, 0, -1, 0, 0, -1, 0, 1,
			0, 0, 0, -1, 1, 0, 0, 0, 0, 1, -1, 0, 0, -1, 0, 0, 1, 0, 0, -1, 0, 1, 0, 0, 0, -1, 1, 0, 2, 1, 1, 0, 0, 1, 1, 1, -1,
			0, 1, 1, 1, 0, -1, 0, 2, 1, 0, 1, 0, 1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 0, 2, 0, 1, 1, 0, 1, -1, 1, 1, 0, 1, 0, 1, 1, -1,
			0, 2, 1, 0, 0, 1, 1, 1, -1, 0, 1, 1, 1, 0, -1, 1, 0, 2, 0, 1, 0, 1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1, 0, 2, 0, 0, 1, 1,
			1, -1, 0, 1, 1, 1, 0, -1, 1, 1, 1, 4, 2, 1, 1, 0, 4, 1, 2, 1, 0, 4, 1, 1, 2, 0, 1, 4, 2, 1, 0, 1, 4, 1, 2, 0, 1, 4, 1,
			1, 0, 2, 1, 4, 2, 0, 1, 1, 4, 1, 0, 2, 1, 4, 1, 0, 1, 2, 1, 4, 0, 2, 1, 1, 4, 0, 1, 2, 1, 4, 0, 1, 1, 2, 1, 2, 1, 1,
			0, 0, 3, 2, 1, 0, 0, 3, 1, 2, 0, 0, 1, 2, 1, 0, 1, 0, 3, 2, 0, 1, 0, 3, 1, 0, 2, 0, 1, 2, 0, 1, 1, 0, 3, 0, 2, 1, 0,
			3, 0, 1, 2, 0, 1, 2, 1, 0, 0, 1, 3, 2, 0, 0, 1, 3, 1, 0, 0, 2, 1, 2, 0, 1, 0, 1, 3, 0, 2, 0, 1, 3, 0, 1, 0, 2, 1, 2,
			0, 0, 1, 1, 3, 0, 0, 2, 1, 3, 0, 0, 1, 2, 2, 3, 1, 1, 1, 0, 2, 1, 1, 1, -1, 2, 2, 0, 0, 0, 2, 3, 1, 1, 0, 1, 2, 1, 1,
			-1, 1, 2, 2, 0, 0, 0, 2, 3, 1, 0, 1, 1, 2, 1, -1, 1, 1, 2, 2, 0, 0, 0, 2, 3, 1, 1, 1, 0, 2, 1, 1, 1, -1, 2, 0, 2, 0,
			0, 2, 3, 1, 1, 0, 1, 2, 1, 1, -1, 1, 2, 0, 2, 0, 0, 2, 3, 0, 1, 1, 1, 2, -1, 1, 1, 1, 2, 0, 2, 0, 0, 2, 3, 1, 1, 1, 0,
			2, 1, 1, 1, -1, 2, 0, 0, 2, 0, 2, 3, 1, 0, 1, 1, 2, 1, -1, 1, 1, 2, 0, 0, 2, 0, 2, 3, 0, 1, 1, 1, 2, -1, 1, 1, 1, 2,
			0, 0, 2, 0, 2, 3, 1, 1, 0, 1, 2, 1, 1, -1, 1, 2, 0, 0, 0, 2, 2, 3, 1, 0, 1, 1, 2, 1, -1, 1, 1, 2, 0, 0, 0, 2, 2, 3, 0,
			1, 1, 1, 2, -1, 1, 1, 1, 2, 0, 0, 0, 2, 2, 1, 1, 1, -1, 0, 1, 1, 1, 0, -1, 0, 0, 0, 0, 0, 2, 1, 1, -1, 1, 0, 1, 1, 0,
			1, -1, 0, 0, 0, 0, 0, 2, 1, -1, 1, 1, 0, 1, 0, 1, 1, -1, 0, 0, 0, 0, 0, 2, 1, 1, -1, 0, 1, 1, 1, 0, -1, 1, 0, 0, 0, 0,
			0, 2, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1, 0, 0, 0, 0, 0, 2, 1, -1, 0, 1, 1, 1, 0, -1, 1, 1, 0, 0, 0, 0, 0, 2, 1, 1, 1, -1,
			0, 1, 1, 1, 0, -1, 2, 2, 0, 0, 0, 2, 1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 2, 2, 0, 0, 0, 2, 1, 1, -1, 0, 1, 1, 1, 0, -1, 1,
			2, 2, 0, 0, 0, 2, 1, 1, 1, -1, 0, 1, 1, 1, 0, -1, 2, 0, 2, 0, 0, 2, 1, -1, 1, 1, 0, 1, 0, 1, 1, -1, 2, 0, 2, 0, 0, 2,
			1, -1, 1, 0, 1, 1, 0, 1, -1, 1, 2, 0, 2, 0, 0, 2, 1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 2, 0, 0, 2, 0, 2, 1, -1, 1, 1, 0, 1,
			0, 1, 1, -1, 2, 0, 0, 2, 0, 2, 1, -1, 0, 1, 1, 1, 0, -1, 1, 1, 2, 0, 0, 2, 0, 2, 1, 1, -1, 0, 1, 1, 1, 0, -1, 1, 2, 0,
			0, 0, 2, 2, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1, 2, 0, 0, 0, 2, 2, 1, -1, 0, 1, 1, 1, 0, -1, 1, 1, 2, 0, 0, 0, 2, 3, 1, 1,
			0, 0, 0, 2, 2, 0, 0, 0, 2, 1, 1, 1, -1, 3, 1, 0, 1, 0, 0, 2, 0, 2, 0, 0, 2, 1, 1, 1, -1, 3, 1, 0, 0, 1, 0, 2, 0, 0, 2,
			0, 2, 1, 1, 1, -1, 3, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 2, 1, 1, -1, 1, 3, 1, 0, 1, 0, 0, 2, 0, 2, 0, 0, 2, 1, 1, -1, 1,
			3, 1, 0, 0, 0, 1, 2, 0, 0, 0, 2, 2, 1, 1, -1, 1, 3, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 2, 1, -1, 1, 1, 3, 1, 0, 0, 1, 0, 2,
			0, 0, 2, 0, 2, 1, -1, 1, 1, 3, 1, 0, 0, 0, 1, 2, 0, 0, 0, 2, 2, 1, -1, 1, 1, 3, 1, 0, 1, 0, 0, 2, 0, 2, 0, 0, 2, -1,
			1, 1, 1, 3, 1, 0, 0, 1, 0, 2, 0, 0, 2, 0, 2, -1, 1, 1, 1, 3, 1, 0, 0, 0, 1, 2, 0, 0, 0, 2, 2, -1, 1, 1, 1, 3, 3, 2, 1,
			0, 0, 3, 1, 2, 0, 0, 4, 1, 1, 1, 1, 3, 3, 2, 0, 1, 0, 3, 1, 0, 2, 0, 4, 1, 1, 1, 1, 3, 3, 0, 2, 1, 0, 3, 0, 1, 2, 0,
			4, 1, 1, 1, 1, 3, 3, 2, 0, 0, 1, 3, 1, 0, 0, 2, 4, 1, 1, 1, 1, 3, 3, 0, 2, 0, 1, 3, 0, 1, 0, 2, 4, 1, 1, 1, 1, 3, 3,
			0, 0, 2, 1, 3, 0, 0, 1, 2, 4, 1, 1, 1, 1, 3, 3, 2, 1, 0, 0, 3, 1, 2, 0, 0, 2, 1, 1, 1, -1, 3, 3, 2, 0, 1, 0, 3, 1, 0,
			2, 0, 2, 1, 1, 1, -1, 3, 3, 0, 2, 1, 0, 3, 0, 1, 2, 0, 2, 1, 1, 1, -1, 3, 3, 2, 1, 0, 0, 3, 1, 2, 0, 0, 2, 1, 1, -1,
			1, 3, 3, 2, 0, 0, 1, 3, 1, 0, 0, 2, 2, 1, 1, -1, 1, 3, 3, 0, 2, 0, 1, 3, 0, 1, 0, 2, 2, 1, 1, -1, 1, 3, 3, 2, 0, 1, 0,
			3, 1, 0, 2, 0, 2, 1, -1, 1, 1, 3, 3, 2, 0, 0, 1, 3, 1, 0, 0, 2, 2, 1, -1, 1, 1, 3, 3, 0, 0, 2, 1, 3, 0, 0, 1, 2, 2,
			1, -1, 1, 1, 3, 3, 0, 2, 1, 0, 3, 0, 1, 2, 0, 2, -1, 1, 1, 1, 3, 3, 0, 2, 0, 1, 3, 0, 1, 0, 2, 2, -1, 1, 1, 1, 3, 3,
			0, 0, 2, 1, 3, 0, 0, 1, 2, 2, -1, 1, 1, 1
		];

		// helper classes

		function shuffleSeed(seed) {
			var newSeed = new Uint32Array(1);
			newSeed[0] = seed[0] * 1664525 + 1013904223;
			return newSeed;
		}

		function Contribution2(multiplier, xsb, ysb) {
			this.dx = -xsb - multiplier * con.SQUISH_2D;
			this.dy = -ysb - multiplier * con.SQUISH_2D;
			this.xsb = xsb;
			this.ysb = ysb;
		}

		function Contribution3(multiplier, xsb, ysb, zsb) {
			this.dx = -xsb - multiplier * con.SQUISH_3D;
			this.dy = -ysb - multiplier * con.SQUISH_3D;
			this.dz = -zsb - multiplier * con.SQUISH_3D;
			this.xsb = xsb;
			this.ysb = ysb;
			this.zsb = zsb;
		}

		function Contribution4(multiplier, xsb, ysb, zsb, wsb) {
			this.dx = -xsb - multiplier * con.SQUISH_4D;
			this.dy = -ysb - multiplier * con.SQUISH_4D;
			this.dz = -zsb - multiplier * con.SQUISH_4D;
			this.dw = -wsb - multiplier * con.SQUISH_4D;
			this.xsb = xsb;
			this.ysb = ysb;
			this.zsb = zsb;
			this.wsb = wsb;
		}

		// initialize

		var contributions2D = [];
		for (var i = 0; i < con.p2D.length; i += 4) {
			var baseSet = con.base2D[con.p2D[i]];
			var previous = null;
			var current = null;
			for (var k = 0; k < baseSet.length; k += 3) {
				current = new Contribution2(baseSet[k], baseSet[k + 1], baseSet[k + 2]);
				if (previous === null)
					contributions2D[i / 4] = current;
				else
					previous.next = current;
				previous = current;
			}
			current.next = new Contribution2(con.p2D[i + 1], con.p2D[i + 2], con.p2D[i + 3]);
		}
		this.lookup2D = [];
		for (var i = 0; i < con.lookupPairs2D.length; i += 2) {
			this.lookup2D[con.lookupPairs2D[i]] = contributions2D[con.lookupPairs2D[i + 1]];
		}
		var contributions3D = [];
		for (var i = 0; i < con.p3D.length; i += 9) {
			var baseSet = con.base3D[con.p3D[i]];
			var previous = null;
			var current = null;
			for (var k = 0; k < baseSet.length; k += 4) {
				current = new Contribution3(baseSet[k], baseSet[k + 1], baseSet[k + 2], baseSet[k + 3]);
				if (previous === null)
					contributions3D[i / 9] = current;
				else
					previous.next = current;
				previous = current;
			}
			current.next = new Contribution3(con.p3D[i + 1], con.p3D[i + 2], con.p3D[i + 3], con.p3D[i + 4]);
			current.next.next = new Contribution3(con.p3D[i + 5], con.p3D[i + 6], con.p3D[i + 7], con.p3D[i + 8]);
		}
		this.lookup3D = [];
		for (var i = 0; i < con.lookupPairs3D.length; i += 2) {
			this.lookup3D[con.lookupPairs3D[i]] = contributions3D[con.lookupPairs3D[i + 1]];
		}
		var contributions4D = [];
		for (var i = 0; i < con.p4D.length; i += 16) {
			var baseSet = con.base4D[con.p4D[i]];
			var previous = null;
			var current = null;
			for (var k = 0; k < baseSet.length; k += 5) {
				current = new Contribution4(baseSet[k], baseSet[k + 1], baseSet[k + 2], baseSet[k + 3], baseSet[k + 4]);
				if (previous === null)
					contributions4D[i / 16] = current;
				else
					previous.next = current;
				previous = current;
			}
			current.next = new Contribution4(con.p4D[i + 1], con.p4D[i + 2], con.p4D[i + 3], con.p4D[i + 4], con.p4D[i + 5]);
			current.next.next = new Contribution4(con.p4D[i + 6], con.p4D[i + 7], con.p4D[i + 8], con.p4D[i + 9], con.p4D[i + 10]);
			current.next.next.next = new Contribution4(con.p4D[i + 11], con.p4D[i + 12], con.p4D[i + 13], con.p4D[i + 14], con.p4D[i + 15]);
		}
		this.lookup4D = [];
		for (var i = 0; i < con.lookupPairs4D.length; i += 2) {
			this.lookup4D[con.lookupPairs4D[i]] = contributions4D[con.lookupPairs4D[i + 1]];
		}

		// end initialize

		this.perm = new Uint8Array(256);
		this.perm2D = new Uint8Array(256);
		this.perm3D = new Uint8Array(256);
		this.perm4D = new Uint8Array(256);
		var source = new Uint8Array(256);
		for (var i = 0; i < 256; i++)
			source[i] = i;
		var seed = new Uint32Array(1);
		seed[0] = clientSeed;
		seed = shuffleSeed(shuffleSeed(shuffleSeed(seed)));
		for (var i = 255; i >= 0; i--) {
			seed = shuffleSeed(seed);
			var r = new Uint32Array(1);
			r[0] = (seed[0] + 31) % (i + 1);
			if (r[0] < 0)
				r[0] += (i + 1);
			this.perm[i] = source[r[0]];
			this.perm2D[i] = this.perm[i] & 0x0E;
			this.perm3D[i] = (this.perm[i] % 24) * 3;
			this.perm4D[i] = this.perm[i] & 0xFC;
			source[r[0]] = source[i];
		}

		this.simplex1D = function(x) {
			return that.simplex2D(x, 1);
		}

		this.simplex2D = function (x, y) {
			var stretchOffset = (x + y) * con.STRETCH_2D;
			var _a = [x + stretchOffset, y + stretchOffset], xs = _a[0], ys = _a[1];
			var _b = [Math.floor(xs), Math.floor(ys)], xsb = _b[0], ysb = _b[1];
			var squishOffset = (xsb + ysb) * con.SQUISH_2D;
			var _c = [x - (xsb + squishOffset), y - (ysb + squishOffset)], dx0 = _c[0], dy0 = _c[1];
			var _d = [xs - xsb, ys - ysb], xins = _d[0], yins = _d[1];
			var inSum = xins + yins;
			var hashVals = new Uint32Array(4);
			hashVals[0] = xins - yins + 1;
			hashVals[1] = inSum;
			hashVals[2] = inSum + yins;
			hashVals[3] = inSum + xins;
			var hash = hashVals[0] | (hashVals[1] << 1) | (hashVals[2] << 2) | (hashVals[3] << 4);
			var c = that.lookup2D[hash];
			var value = 0.0;
			while (typeof c !== 'undefined') {
				var _e = [dx0 + c.dx, dy0 + c.dy], dx = _e[0], dy = _e[1];
				var attn = 2 - dx * dx - dy * dy;
				if (attn > 0) {
					var _f = [xsb + c.xsb, ysb + c.ysb], px = _f[0], py = _f[1];
					var i = that.perm2D[(that.perm[px & 0xFF] + py) & 0xFF];
					var valuePart = con.gradients2D[i] * dx + con.gradients2D[i + 1] * dy;
					attn *= attn;
					value += attn * attn * valuePart;
				}
				c = c.next;
			}
			return value * con.NORM_2D;
		};

		this.simplex3D = function (x, y, z) {
			var stretchOffset = (x + y + z) * con.STRETCH_3D;
			var _a = [x + stretchOffset, y + stretchOffset, z + stretchOffset], xs = _a[0], ys = _a[1], zs = _a[2];
			var _b = [Math.floor(xs), Math.floor(ys), Math.floor(zs)], xsb = _b[0], ysb = _b[1], zsb = _b[2];
			var squishOffset = (xsb + ysb + zsb) * con.SQUISH_3D;
			var _c = [x - (xsb + squishOffset), y - (ysb + squishOffset), z - (zsb + squishOffset)], dx0 = _c[0], dy0 = _c[1], dz0 = _c[2];
			var _d = [xs - xsb, ys - ysb, zs - zsb], xins = _d[0], yins = _d[1], zins = _d[2];
			var inSum = xins + yins + zins;
			var hashVals = new Uint32Array(7);
			hashVals[0] = yins - zins + 1;
			hashVals[1] = xins - yins + 1;
			hashVals[2] = xins - zins + 1;
			hashVals[3] = inSum;
			hashVals[4] = inSum + zins;
			hashVals[5] = inSum + yins;
			hashVals[6] = inSum + xins;
			var hash = hashVals[0] | hashVals[1] << 1 | hashVals[2] << 2 | hashVals[3] << 3 | hashVals[4] << 5 |
				hashVals[5] << 7 | hashVals[6] << 9;
			var c = that.lookup3D[hash];
			var value = 0.0;
			while (typeof c !== 'undefined') {
				var _e = [dx0 + c.dx, dy0 + c.dy, dz0 + c.dz], dx = _e[0], dy = _e[1], dz = _e[2];
				var attn = 2 - dx * dx - dy * dy - dz * dz;
				if (attn > 0) {
					var _f = [xsb + c.xsb, ysb + c.ysb, zsb + c.zsb], px = _f[0], py = _f[1], pz = _f[2];
					var i = that.perm3D[(that.perm[(that.perm[px & 0xFF] + py) & 0xFF] + pz) & 0xFF];
					var valuePart = con.gradients3D[i] * dx + con.gradients3D[i + 1] * dy + con.gradients3D[i + 2] * dz;
					attn *= attn;
					value += attn * attn * valuePart;
				}
				c = c.next;
			}
			return value * con.NORM_3D;
		};

		this.simplex4D = function (x, y, z, w) {
			var stretchOffset = (x + y + z + w) * con.STRETCH_4D;
			var _a = [x + stretchOffset, y + stretchOffset, z + stretchOffset, w + stretchOffset], xs = _a[0], ys = _a[1], zs = _a[2], ws = _a[3];
			var _b = [Math.floor(xs), Math.floor(ys), Math.floor(zs), Math.floor(ws)], xsb = _b[0], ysb = _b[1], zsb = _b[2], wsb = _b[3];
			var squishOffset = (xsb + ysb + zsb + wsb) * con.SQUISH_4D;
			var dx0 = x - (xsb + squishOffset);
			var dy0 = y - (ysb + squishOffset);
			var dz0 = z - (zsb + squishOffset);
			var dw0 = w - (wsb + squishOffset);
			var _c = [xs - xsb, ys - ysb, zs - zsb, ws - wsb], xins = _c[0], yins = _c[1], zins = _c[2], wins = _c[3];
			var inSum = xins + yins + zins + wins;
			var hashVals = new Uint32Array(11);
			hashVals[0] = zins - wins + 1;
			hashVals[1] = yins - zins + 1;
			hashVals[2] = yins - wins + 1;
			hashVals[3] = xins - yins + 1;
			hashVals[4] = xins - zins + 1;
			hashVals[5] = xins - wins + 1;
			hashVals[6] = inSum << 6;
			hashVals[7] = inSum + wins;
			hashVals[8] = inSum + zins;
			hashVals[9] = inSum + yins;
			hashVals[10] = inSum + xins;
			var hash = hashVals[0] | hashVals[1] << 1 | hashVals[2] << 2 | hashVals[3] << 3 | hashVals[4] << 4 | hashVals[5] << 5 |
				hashVals[6] << 6 | hashVals[7] << 8 | hashVals[8] << 11 | hashVals[9] << 14 | hashVals[10] << 17;
			var c = that.lookup4D[hash];
			var value = 0.0;
			while (typeof c !== 'undefined') {
				var _d = [dx0 + c.dx, dy0 + c.dy, dz0 + c.dz, dw0 + c.dw], dx = _d[0], dy = _d[1], dz = _d[2], dw = _d[3];
				var attn = 2 - dx * dx - dy * dy - dz * dz - dw * dw;
				if (attn > 0) {
					var _e = [xsb + c.xsb, ysb + c.ysb, zsb + c.zsb, wsb + c.wsb], px = _e[0], py = _e[1], pz = _e[2], pw = _e[3];
					var i = that.perm4D[(that.perm[(that.perm[(that.perm[px & 0xFF] + py) & 0xFF] + pz) & 0xFF] + pw) & 0xFF];
					var valuePart = con.gradients4D[i] * dx + con.gradients4D[i + 1] * dy + con.gradients4D[i + 2] * dz + con.gradients4D[i + 3] * dw;
					attn *= attn;
					value += attn * attn * valuePart;
				}
				c = c.next;
			}
			return value * con.NORM_4D;
		};

	}//-13.9

/*--
zim.Point = function(x, y, z, q, r, s, t, u, v, w)

Point
zim class

DESCRIPTION
Stores x, y, z, q, r, s, t, u, v, w properties.
See also createjs.Point for a Point class with more features.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var point = new Point(100, 100);
zog(point.x, point.y); // 100, 100
END EXAMPLE

PARAMETERS
x - (default 0) the x value of the point
y - (default 0) the y value of the point
z - (default 0) the z value of the point - probably not used
q - (default 0) the q value of the point - very probably not used
r - (default 0) the r value of the point - very probably not used
s - (default 0) the s value of the point - very probably not used
t - (default 0) the t value of the point - very probably not used
u - (default 0) the u value of the point - very probably not used
v - (default 0) the v value of the point - very probably not used
w - (default 0) the w value of the point - very probably not used

PROPERTIES
x - the x value of the point
y - the y value of the point
z - the z value of the point - probably not used
q - the q value of the point - very probably not used
r - the r value of the point - very probably not used
s - the s value of the point - very probably not used
t - the t value of the point - very probably not used
u - the u value of the point - very probably not used
v - the v value of the point - very probably not used
w - the w value of the point - very probably not used
--*///+13.45
	zim.Point = function(x, y, z, q, r, s, t, u, v, w) {
		z_d("13.45");
		if (zot(x)) x = 0;
		if (zot(y)) y = 0;
		if (zot(z)) z = 0;
		if (zot(q)) q = 0;
		if (zot(r)) r = 0;
		if (zot(s)) s = 0;
		if (zot(t)) t = 0;
		if (zot(u)) u = 0;
		if (zot(v)) v = 0;
		if (zot(w)) w = 0;
		this.x = x;
		this.y = y;
		this.z = z;
		this.q = q;
		this.r = r;
		this.s = s;
		this.t = t;
		this.u = u;
		this.v = v;
		this.w = w;
	}//-13.45

/*--
zim.Boundary = function(x, y, width, height)

Boundary
zim class

DESCRIPTION
Stores the data for a rectangle with x, y, width and height.
Can be used with ZIM drag(), gesture() for boundaries
and the Physics module for world boundary.

NOTE: A createjs.Rectangle or an object {} with x, y, width and height properties can also be used
Boundary was introduced to reduce confusion over having a ZIM Rectangle (Shape) and a CreateJS Rectangle (data)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
new Circle(100, blue)
	.center()
	.drag(new Boundary(0,0,stageW,stageH));
// note: drag and gesture used to have rect parameters
// these have now been depreciated and replaced with boundary parameters

// CONTRACT
// the drag() boundary contains the registration point
// note: the gesture() boundary contains the whole shape of the object
// here, we keep the circle inside the stage by contracting the Boundary by the radius
var radius = 100;
new Circle(radius, red)
	.center()
	.drag(new Boundary(0,0,stageW,stageH).contract(radius));
END EXAMPLE

PARAMETERS
x - the x position of the Boundary
y - the y position of the Boundary
width - the width of the Boundary
height - the height of the Boundary

PROPERTIES
x - the x position of the Boundary
y - the y position of the Boundary
width - the width of the Boundary
height - the height of the Boundary

METHODS
contract(number|x, y, width, height) - number of pixels to make the Boundary smaller
	passing in a single number will contract this on all sides
	passing in two numbers will contract from horizontal and vertical accordingly
	passing in four numbers will contract from the sides accordingly
	note: to expand pass in a negative number
	returns object for chaining
--*///+13.46
	zim.Boundary = function(x, y, width, height) {
		z_d("13.46");
		if (zot(x) || zot(y) || zot(width) || zot(height)) return;
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
		this.contract = function(a,b,c,d) {
			if (zot(a)) return this;
			if (zot(b)) b = a;
			if (zot(c)) {
				c = a*2;
			} else {
				c = c+a;
			}
			if (zot(d)) {
				d = b*2;
			} else {
				d = d+b;
			}
			this.x += a;
			this.y += b;
			this.width -= c;
			this.height -= d;
			return this;
		}
	}//-13.46


/*--
zim.Damp = function(startValue, damp)

Damp
zim class

DESCRIPTION
Damping emulates things slowing down due to friction.
The movement heads towards the right value and looks organic.
This is similar if not the same as easing out when tweening.
Create your Damp object outside an interval or Ticker
then inside an interval or ticker call the convert method.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var d = new Damp(parameters);
setInterval(function() {
	dampedValue = d.convert(desiredValue);
}, 100);
END EXAMPLE

you would then apply that desired value to a property such as x or y or scale
if you want to do both x and y then you need two Damp objects
and two convert calls (you can do both in one interval or ticker)

EXAMPLE
var circle = new Circle();
circle.center(stage);
var dampX = new Damp(circle.x);
var dampY = new Damp(circle.y);
// start moving once mouse enters stage
// this event will only run once (the last parameter is true)
stage.on("stagemousemove", start, null, true);
function start(e) {
	Ticker.add(function() {
		circle.x = dampX.convert(e.stageX);
		circle.y = dampY.convert(e.stageY);
	}, stage);
}
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
startValue - (default 0) start object at this value and then start damping
damp - (default .1) the damp value with 1 being no damping and 0 being no movement

METHODS
convert(value) - converts a value into a damped value
immediate(value) - immediately goes to value and returns the Damp object

PROPERTIES
damp - can dynamically change the damping (usually just pass it in as a parameter to start)
lastValue - setting this would go immediately to this value (would not normally use)
--*///+14
	zim.Damp = function(startValue, damp) {
		z_d("14");
		var sig = "startValue, damp";
		var duo; if (duo = zob(zim.Damp, arguments, sig, this)) return duo;
		this.lastValue = (zot(startValue)) ? 0 : startValue;
		this.damp = (zot(damp)) ? .1 : damp;
	}
	zim.Damp.prototype.convert = function(desiredValue) {
		return this.lastValue = this.lastValue + (desiredValue - this.lastValue) * this.damp;
	}
	zim.Damp.prototype.immediate = function(desiredValue) {
		this.lastValue = desiredValue;
		return this;
	}//-14

/*--
zim.Proportion = function(baseMin, baseMax, targetMin, targetMax, factor, targetRound, clamp)

Proportion
zim class

DESCRIPTION
Proportion converts an input value to an output value on a different scale.
(sometimes called a map() function)
For instance, like a slider controlling the scale of an object or sound volume.
Make a Proportion object and then in an interval, ticker or event,
convert the base value to the target value using the convert method.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
frame.loadAssets("mySound.mp3");
frame.on("complete", function() {
	var sound = frame.asset("mySound.mp3").play();
	var p = new Proportion(0, 10, 0, 1);
	var dial = new Dial(); // default range of 0 to 10
	dial.currentValue = 10;
	dial.on("change", function(){
		sound.volume = p.convert(dial.currentValue);
	}); // end of dial change
}); // end sound loaded
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
baseMin - min for the input scale (say x value)
baseMax - max for the input scale (say x value)
targetMin - (default 0) min for the output scale (say volume)
targetMax - (default 1) max for the output scale (say volume)
factor - (default 1) is going the same direction and -1 is going in opposite direction
targetRound - (default false) set to true to round the converted number
clamp - (default true) set to false to let results go outside min and max range

METHODS
convert(input) - will return the output property (for instance, a volume)

NOTE: the object always starts by assuming baseMin as baseValue
just call the convert method right away if you want it to start at a different baseValue
for instance, if your slider went from 100 to 500 and you want to start at half way
make the object and call p.convert(300); on the next line
--*///+15
	zim.Proportion = function(baseMin, baseMax, targetMin, targetMax, factor, targetRound, clamp) {
		var sig = "baseMin, baseMax, targetMin, targetMax, factor, targetRound, clamp";
		var duo; if (duo = zob(zim.Proportion, arguments, sig, this)) return duo;
		z_d("15");
		// factor - set to 1 for increasing and -1 for decreasing
		// round - true to round results to whole number
		if (zot(targetMin)) targetMin = 0;
		if (zot(targetMax)) targetMax = 1;
		if (zot(factor)) factor = 1;
		if (zot(targetRound)) targetRound = false;
		if (zot(clamp)) clamp = true;

		// proportion
		var baseAmount;
		var proportion;
		var targetAmount;

		baseAmount = baseMin; // just start at the min otherwise call immediate(baseValue);

		this.convert = function(baseAmount) {
			if (isNaN(baseAmount) || (baseMax-baseMin==0)) {return;}
			if (clamp) {
				baseAmount = Math.max(baseAmount, baseMin);
				baseAmount = Math.min(baseAmount, baseMax);
			}
			proportion = (baseAmount - baseMin) / (baseMax - baseMin);
			if (factor > 0) {
				targetAmount = targetMin + (targetMax-targetMin) * proportion;
			} else {
				targetAmount = targetMax - (targetMax-targetMin) * proportion;
			}
			if (targetRound) {targetAmount = Math.round(targetAmount);}
			return targetAmount;
		}
	}//-15

/*--
zim.ProportionDamp = function(baseMin, baseMax, targetMin, targetMax, damp, factor, targetRound, clamp)

ProportionDamp
zim class

DESCRIPTION
ProportionDamp converts an input value to an output value on a different scale with damping.
Works like Proportion Class but with a damping parameter.
Damping needs constant calculating so do not put in mousemove event.
The below example scales the circle based on the mouse height.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage); // center method added in ZIM 4TH
var pd = new ProportionDamp(0, stageH, 0, 5, .2);
Ticker.add(function() {
	circle.sca(pd.convert(stage.mouseH)); // scale method added in ZIM 4TH
}, stage);
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
baseMin - min for the input scale (say x value)
baseMax - max for the input scale (say x value)
targetMin - (default 0) min for the output scale (say volume)
targetMax - (default 1) max for the output scale (say volume)
damp - (default .1) the damp value with 1 being no damping and 0 being no movement
factor (default 1) is going the same direction and -1 is going in opposite direction
targetRound (default false) set to true to round the converted number

METHODS
convert(input) - converts a base value to a target value
immediate(input) - immediately sets the target value (no damping) and returns the ProportionDamp object
dispose() - clears interval

PROPERTIES
damp - can adjust this dynamically (usually just pass it in as a parameter to start)

NOTE: the object always starts by assuming baseMin as baseValue
if you want to start or go to an immediate value without easing then
call the pd.immediate(baseValue) method with your desired baseValue (not targetValue)
--*///+16
	zim.ProportionDamp = function(baseMin, baseMax, targetMin, targetMax, damp, factor, targetRound, clamp) {
		var sig = "baseMin, baseMax, targetMin, targetMax, damp, factor, targetRound, clamp";
		var duo; if (duo = zob(zim.ProportionDamp, arguments, sig, this)) return duo;
		z_d("16");
		// damp - can be changed via damp get/set method property
		// factor - set to 1 for increasing and -1 for decreasing
		// round - true to round results to whole number
		// zot() is found in danzen.js (the z version of not)
		if (zot(targetMin)) targetMin = 0;
		if (zot(targetMax)) targetMax = 1;
		if (zot(damp)) damp = .1;
		if (zot(factor)) factor = 1;
		if (zot(targetRound)) targetRound = false;
		if (zot(clamp)) clamp = true;

		this.damp = damp; // want to expose as a property we can change
		var that = this;

		// proportion
		var baseAmount;
		var proportion;
		var targetDifference;
		var targetAmount;

		// damping
		var differenceAmount;
		var desiredAmount=0;
		var lastAmount = 0;

		baseAmount = baseMin; // just start at the min otherwise call immediate(baseValue);
		lastAmount = targetMin;

		var interval = setInterval(calculate, 20);

		function calculate() {
			if (isNaN(baseAmount) || (baseMax-baseMin==0)) {return;}

			if (clamp) {
				baseAmount = Math.max(baseAmount, baseMin);
				baseAmount = Math.min(baseAmount, baseMax);
			}

			proportion = (baseAmount - baseMin) / (baseMax - baseMin);
			targetDifference = targetMax - targetMin;

			if (factor > 0) {
				targetAmount = targetMin + targetDifference * proportion;
			} else {
				targetAmount = targetMax - targetDifference * proportion;
			}

			desiredAmount = targetAmount;
			differenceAmount = desiredAmount - lastAmount;
			lastAmount += differenceAmount*that.damp;
		}

		this.immediate = function(n) {
			that.convert(n);
			calculate();
			lastAmount = targetAmount;
			if (targetRound) {lastAmount = Math.round(lastAmount);}
			return that;
		}

		this.convert = function(n) {
			baseAmount = n;
			if (targetRound) {
				return Math.round(lastAmount);
			} else {
				return lastAmount;
			}
		}

		this.dispose = function() {
			clearInterval(interval);
			return true;
		}
	}//-16

/*--
zim.Dictionary = function(unique)

Dictionary
zim class

DESCRIPTION
An object that uses objects as keys to give values.
Similar to an object literal with properties except the property names are objects instead of strings.
JavaScript currently does not have a dictionary, but other languages do.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var o = {test:"test"};
var f = function(w) {zog(w)};
var c = new Circle();
var d = new Dictionary();
d.add(o, 1); d.add(f, 2); d.add(c, f);
zog(d.at(o)); // 1
zog(d.at(f)); // 2
d.at(c)("hello"); // hello
d.remove(o); // to clear o
zog(d.length); // 2
END EXAMPLE

EXAMPLE
var d = new Dictionary();
d.add(circle, "one");
d.add(circle, "two");
zog(d.at(circle)); // two - just the latest but "one" is still there
for (var i=0; i<d.length; i++) {
	if (d.objects[i] == circle) zog(d.values[i]); // one then two
}
// note, loop backwards to clear values at a key
END EXAMPLE

EXAMPLE
// with unique property add(key, val) removes the last val at that key
var d = new Dictionary(true);
d.add(circle, "one");
d.add(circle, "two");
zog(d.at(circle)); // two - and now only two is there
for (var i=0; i<d.length; i++) {
	if (d.objects[i] == circle) zog(d.values[i]); // two
}
// note, now d.remove(key) removes that unique entry for the key
END EXAMPLE

PARAMETERS
unique (default false) - set to true to only accept a single entry (the last added) for a key

METHODS
add(object, value) - adds a value that can be retrieved by an object reference
	if unique is false, this will not overwrite previous entries at the object key
	if unique is true, this will overwrite previous entries at the object key
	value is optional and will default to true
at(object) - retrieves the last value stored at the object (or returns null if not there)
remove(object) - removes the last value at the object from the Dictionary returns boolean success
clear() - removes all objects from Dictionary - returns object for chaining
dispose() - deletes Dictionary object

PROPERTIES
length - the number of items in the Dictionary
unique - whether the dictionary will overwrite values (going from false to true will not delete previous values)
objects - array of keys
values - array of values synched to keys
--*///+17
	zim.Dictionary = function(unique) {
		z_d("17");
		this.length = 0;
		this.unique = unique;
		var objects = this.objects = []; // store objects and values in synched arrays
		var values = this.values = [];

		this.add = function(o,v) {
			if (zot(o)) return;
			if (zot(v)) v = true;
			if (this.unique) this.remove(o);
			objects.push(o);
			values.push(v);
			this.length++;
		}

		this.at = function(o) {
			if (zot(o)) return;
			var i = objects.indexOf(o);
			if (i > -1) return values[i];
			return null;
		}

		this.remove = function(o) {
			if (zot(o)) return false;
			var i = objects.indexOf(o);
			if (i > -1) {
				objects.splice(i,1);
				values.splice(i,1);
				this.length--
				return true;
			} else {
				return false;
			}
		}

		this.clear = function() {
			objects = this.objects = []; // store objects and values in synched arrays
			values = this.values = [];
			this.length = null;
			return this;
		}

		this.dispose = function() {
			objects = null;
			values = null;
			this.length = null;
			return true;
		}
	}//-17

/*--
zim.Hierarchy = function(input)

Hierarchy
zim class

DESCRIPTION
A hierarchy is a nested structure for organized data (like XML).
For instance, Hierarchy manages the accordion feature of ZIM List()
where the list can be expanded and collapsed to show nested sections.

HOW IT WORKS
There are two formats that can be used as input.
Each will create a data property in final hierarchy format.

A. The SIMPLE input is an easier format to write but
has a limitation in that identifiers must be strings.
The Hierarchy replaces the identifiers with sequential ids
and stores the original identifiers as an obj property.

B. The COMPLEX input starts off like the final hierarchy data
which has the advantage of storing any type of object in the hierarchy.
The disadvantage is the ids must be hand coded and it is longer.
The Hierarchy will add level, open and opened properties
and create the data property in final hierarchy format.

STRATEGY
The complex format is tricky.
One way to deal with it is to pass in a SIMPLE string version.
Then replace the strings with final objects in the data property.
The Hierarchy class is used internally by ZIM so may not be needed.
However, it will help for custom tree menus, mindmaps, etc.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE - SIMPLE INPUT
// 1. Here is linear input as an array - a boring tree - more like grass!
var simple = [1,2,3,4]; // so we would probably not use Hierarchy for this.

// 2. To hold nested data we use an object literal.
// The properties can hold linear arrays as values
// but if any of the values need to hold more values then use an object literal:
var simple = {
    "linear":[1,2,3,4], // an array is okay as all items are leaf nodes (end nodes)
    "nested":{ // an object literal is required as one or more items hold other items
        "A":[], // this holds nothing (a leaf node) but still needs an empty array
        "B":["one", "two", "three"], // this holds a linear list - all leaf nodes
        "C":{ // this holds another nested list where at least one item holds more
            "LEAF":[],
            "LINEAR":[1,2,3,4],
            "LEAF":[]
        }
    }
}

// pass the object into the input parameter:
var hierarchy = new Hierarchy(simple);
var data = hierarchy.data;

// data will be the following - note it is a more complex format
// but also a form that could hold any type of data as a value
// note the level matches the indent and all values have an id
data = {
    id0:{obj:"linear", level:0, open:false, opened:false, list:{
        id1:{obj:1, level:1},
        id2:{obj:2, level:1},
        id3:{obj:3, level:1},
        id4:{obj:4, level:1}
    }},
    id5:{obj:"nested", level:0, open:false, opened:false, list:{
        id6:{obj:"A", level:1},
        id7:{obj:"B", level:1, open:false, opened:false, list:{
            id8:{obj:"one", level:2},
            id9:{obj:"two", level:2},
            id10:{obj:"three", level:2}
        }},
        id11:{obj:"C", level:1, open:false, opened:false, list:{
            id12:{obj:"LEAF", level:2},
            id13:{obj:"LINEAR", level:2, open:false, opened:false, list:{
                id14:{obj:1, level:3},
                id15:{obj:2, level:3},
                id16:{obj:3, level:3},
                id17:{obj:4, level:3}
            }},
            id18:{obj:"LEAF", level:2}
        }}
    }}
}
END EXAMPLE

EXAMPLE - COMPLEX INPUT
// Here is a sample of passing in the more complex format.
// level, open, opened properties are not required
// they will be added by the Hierarchy.
// Note that there are no arrays in this data format so that each item has an id.
// This could be passed in to the data property of List
// to show an expandable hierarchy of objects rather than strings converted to buttons
var complex = {
	id0:{obj:new Rectangle(), list:{
		id1:{obj:new Triangle()},
		id2:{obj:new Triangle()},
		id3:{obj:new Triangle()}
	}},
	id4:{obj:new Circle(), list:{
		id5:{obj:new Button()},
		id6:{obj:new Button(), list:{
			id7:{obj:new CheckBox()},
			id8:{obj:new CheckBox()},
		}},
		id9:{obj:new Button(), list:{
			id10:{obj:new Label("Leaf")},
			id11:{obj:new Label("Nest"), list:{
				id12:{obj:1},
				id13:{obj:2},
				id14:{obj:3},
				id15:{obj:4},
			}},
			id16:{obj:new Label("Leaf")}
		}}
	}}
};
var hierarchy = new Hierarchy(complex);
var list = new List({list:hierarchy.data, align:"center"}).center();
END EXAMPLE

PARAMETERS
input (default null) - a simple formated input - see the examples
	this will be turned in to a more complex object literal available as the data property
	OR input a complex formated input - similar to the data property output
	but only ids, obj and list properties are required.
	The main purpose of Hierarchy is to create the complex data object
	but passing in a complex format allows objects other than strings to be used.

METHODS
processSimple(input) - enter simple input - returns a Hierarchy data object literal
	this is what the Hierarchy does when a simple input is provided to make its data property
	the input must be in the simple format as described in the SIMPLE example.
processComplex(input) - enter complex input - returns a Hierarchy data object literal
	this is what the Hierarchy does when complex data is provided to make its data property
	processComplex will add level, open, and opened properties to the data
	otherwise data must be in the same format as the final data property - see COMPLEX example
getLinearList(data) - enter final data format, to get a linear list top level objects
	and all open list objects within according to the open property
getLinearIds(data) - enter final data format, to get a linear list top level ids
	and all open lists ids within according to the open property
getData(id) - find the final data format for a given Hierarchy ID - eg. id0, id12, etc.
getNextSibling(id) - gets the id of the next sibling - skipping ids of any children
	will find parents next sibling if last child in parent
	or undefined if last child in hierarchy
getPrevSibling(id) - gets the id of the previous sibling - skipping ids of any children
	will find the parent if first child in parent
	or undefined if first child in hierarchy


PROPERTIES
data - a hierarchy formated object literal with ids that hold object literals
	ids are in the format of id0, id1, id2, etc. numbered in order of creation (top to bottom)
	the object literals have obj, level, open, opened and list properties
	leaf nodes (end nodes) will have {} as its list property value
--*///+17.5
	zim.Hierarchy = function(input) {
		z_d("17.5");
		var that = this;
        if (zot(input)) return;

        that.processSimple = function(list) {
            var count = 0;
            var m = {}
            function makeLevel(list, obj, level) {
                if (list.constructor == {}.constructor) {
                    loop(list, function (key, val) {
                        var newList = {};
                        obj["id"+count] = {obj:key, level:level, open:false, opened:false, list:newList}
                        count++;
                        makeLevel(val, newList, level+1);
                    });
                } else if (Array.isArray(list)) {
                    loop(list, function (val) {
                        var newList = {};
                        obj["id"+count] = {obj:val}
                        count++;
                    });
                }
            }
            makeLevel(input, m, 0);
            return m;
        }
		that.processComplex = function(input) {
			var count = 0;
			function innerFunction(inp, level) {
				loop(inp, function (key, val) {
					val.level = level;
					val.open = false;
					val.opened = false;
					if (val.list) innerFunction(val.list, level+1);
				});
			}
			innerFunction(input, 0);
			return input;
		}

		if (zot(input.id0)) {
			that.data = that.processSimple(input);
		} else {
			that.data = that.processComplex(input);
		}

        that.getLinearList = function(data) {
			if (zot(data)) data = that.data;
            return getLinear(data)[0];
        }
        that.getLinearIDs = function(data) {
			if (zot(data)) data = that.data;
            return getLinear(data)[1];
        }

        function getLinear(data) {
            var linear = [];
            var ids = [];
            function getLevel(data, level) {
                loop(data, function (item, list) {
                    linear.push(list.obj);
                    ids.push(item);
                    if (list.open) getLevel(list.list, level+1);
                })
            }
            getLevel(data, 0);
            return [linear, ids];
        }

        that.getData = function(id) {
            // recursively find the data for id
            var answer;
            function find(obj) {
                loop(obj, function (key, val) {
                    if (key == id) {
                        answer = val;
                        return answer;
                    }
                    find(val.list);
                });
            }
            find(that.data);
            return answer;
        }

        that.getNextSibling = function(id) {
            var current;
            var answer;
            function find(obj) {
                loop(obj, function (key, val) {
                    if (!answer && current) {
                        answer = key;
                        return answer;
                    }
                    if (key == id) {
                        current = val;
                    } else {
                        find(val.list);
                    }
                });
            }
            find(that.data);
            return answer;
        }

		that.getPrevSibling = function(id) {
			var lasts = [];
            function find(obj, lev) {
                loop(obj, function (key, val) {
                    if (key == id) {
                        return lasts[lev] || lasts[lev-1];
                    } else {
						lasts[lev] = key;
                        find(val.list, lev+1);
                    }
                });
            }
            find(that.data, 0);
            return answer;
        }

	}//-17.5

/*--
zim.Pick = function(choices)

Pick
zim class

HISTORY
Pick was originally zik() and ZIM VEE (introduced in ZIM 5) throughout the ZIM documentation
It was so handy that a general (non ZIM centric) name has been given to a class
that can be used by other libraries and languages.
See https://github.com/danzen/Pick for the complete general code and description

DESCRIPTION
Pick() provides a system to handle dynamic parameters.
It does so by providing formats for options and a choose() method to pick from the options.

For example, a particle emitter has an obj parameter to receive what type of particle to emit.
If a particle were randomly chosen from a rectangle, circle or triangle and passed into obj
then the emitter would only emit the randomly chosen particle - say, a bunch of triangles.
What is desired is that the emitter emitt rectangles, circles and triangles randomly.
Pick() provides a way to pass in all three shapes and have the emitter choose each time it emits.

FORMATS
The Pick() formats handle:
	1. a random selection: ["blue", "green", "yellow"] - array format
	2. a random range: {min:10, max:30} - range object format
	3. a series: series(10,20,30) - series format also Pick.series()
	4. a function result: function(){return new Date().minutes} - function format
	5. a normal value: 7 or "hello" - single-value format
	6. a noPick object: {noPick:["real", "array"]} - escape format
	7. a combination: [{min:10, max:20}, 30, 40] - combination format (recursive)

NOTE: the range format gets passed to ZIM rand() directly so see docs there
there are also integer and negative parameters both defaulting to false

PICK LITERAL
Formats are passed in to a Pick() object and the Pick object can be passed to a class or function parameter
In ZIM, the formats may be passed directly into the class or function parameter (as a Pick literal)
	new Circle(new Pick([10,20]), red); // Pick()
	new Circle([10,20], red); // Pick Literal
** The literal can run into conflicts such as for a ZIM corner parameter which accepts an array []
This would be avoided with a system always using Pick() and never the literal.
But... ZIM started off without the formalized Pick - and the literal is shorter ;-)

NOTE: Pick is used internally by by zim.interval, zim.animate, zim.Emitter, zim.Pen, etc.
as well as ZIM Shape basic parameters handy for cloning with Tile, Emitter, etc.

EXAMPLE
var loopCount = new Pick([1,2,3]);
var choice = Pick.choose(loopCount); // 1, 2, or 3

var loopCount = [1,2,3]; // Pick literal
var choice = Pick.choose(loopCount); // 1, 2, or 3

var choice = Pick.choose([1,2,3]); // 1, 2, or 3

var rotation = {min:10, max:20, integer:false, negative:true};
// an example of a Range object - this will give values between -20 and -10 or 10 and 20
// rotation now holds an object as to how to pick its value
// this can be passed into a zim.Emitter() for instance
// which will make multiple copies and rotate them based on Pick.choose()
// or this can be passed into an animation object
// and then into zim.Emitter() for the animate parameter

var emitter = new zim.Emitter({
	obj:new zim.Rectangle(),
	random:{rotation:rotation} // the emitter will use Pick.choose() to pick a rotation for each particle
});

function age() {
	// assuming user.age is some input value that exists
	if (user.age >= 18) return ["a", "b", ["c","d"]];
	else return ["e", "f"];
}
// below will be a, b, c or d if user is 18+ with a and b having more of a chance
// or e or f if not over 18
var show = Pick.choose(age);

// below we randomize the tile colors in the first example
// and make them in color order for the second example
new Tile(new Rectangle(10,10,["blue", "red"]), 10, 10); would randomize colors
new Tile(new Rectangle(10,10,series("blue", "red")), 10, 10); would alternate colors

// here we pass an array through without processing the array:
Pick.choose({noPick:[1,2,3,4,5]}); // result is [1,2,3,4,5]

// a range between one and three seconds - repeating after 3 choices
var pick = new Pick({min:1000, max:3000}).num(3);
interval(pick, function () {console.log("calling");}); // eg. 2.5s, 2.7s, 1.2s, 2.5s, 2.7s, 1.2s, etc.
END EXAMPLE

PARAMETERS
choices - any of the ZIM Pick formats:
	1. an array of choices to choose from randomly
	2. a range object with min and max (integer and negative) properties
	3. a zim.series() or Pick.series() to pick objects in order
	4. a function that returns a result
	5. a single not one of the above that passes through
	6. an escape object with a noPick property
	7. any combination of the above to pick recursively

METHODS
num(number) - a chainable method to limit the number of options until Pick.choose() will repeat like a series
loop(number, call(value, index, total)) - a way to loop through options
 	number is the number of times to loop
	call is a callback function that gets called each loop
	the callback will recieve a value (the choice), the index of the loop and the total (the number parameter)
	inside the function a return is like a continue and a return of any value is like a break in traditional for loops.

STATIC METHODS
Pick.choose(Pick Object or Pick Literal) - gets a value from the Pick object or Pick Literal
	so chooses from a random array, or range, or gets the next in the series, etc.
Pick.rand(a, b, integer, negative) - gets a random number - same as zim.rand()
	a - the first Number for the range
		if a and b are not provided, rand() acts like Math.random()
		if parameter b is not provided, rand will use range 0 to and including a
	b - (default 0) second Number for the range
		it does not matter if a>b or a<b
	integer - (default true) set to false to include decimals in results
		if false, range will include decimals up to but not including the highest number
		if a or b have decimals this is set to false
	negative - (default false) includes the negative range as well as the positive
Pick.series(array|item1, item2, item3, etc.) - same as zim.series()
	returns a function that can be called many times each time returning the next value in the series (eventually looping)
	array|item1 - the first item - or an array of results that will be called in order as the resulting function is called
	item2 - the second item if the first is not an array
	item3 - the third item, etc. to as many items as needed

PROPERTIES
type - the type of object as a String
choices - a reference to the choices object provided as the Pick(choices) parameter
--*///+17.6
	zim.pickCheck = false;
	zim.Pick = function(choices) {
		if (!zim.pickCheck) {z_d("17.6"); zim.pickCheck=true;}
        this.choices = choices;
        this.num = function(num) {
            var s = [];
            for (var i=0; i<num; i++) {s.push(zim.Pick.choose(this))}
            this.choices = zim.Pick.series(s);
            return this;
        }
        var that = this;
        this.loop = function(num, call) {
            var r;
            for (var i=0; i<num; i++) {
                r = call(zim.Pick.choose(that), i, num);
                if (typeof r != 'undefined') return r;
            }
         }
    };
    zim.Pick.prototype.type = "Pick";
    zim.Pick.series = function() {
		// see https://github.com/danzen/Pick for all inclusive class to use in other libraries / languages, etc.
		if (!zim.pickCheck) {z_d("17.6"); zim.pickCheck=true;}
		return zim.series.apply(null, arguments);
    }
    zim.Pick.rand = function(a, b, integer, negative) {
		// see https://github.com/danzen/Pick for all inclusive class to use in other libraries / languages, etc.
		if (!zim.pickCheck) {z_d("17.6"); zim.pickCheck=true;}
		return zim.rand(a, b, integer, negative);
    }
    zim.Pick.choose = function(obj, literal) {
		if (!zim.pickCheck) {z_d("17.6"); zim.pickCheck=true;}
        if (literal == null) literal = true;
		if (obj==null) return obj;
        if (obj.type=="Pick" || literal) {
            var c = obj.choices || obj;
            if (Array.isArray(c)) {
                var val = c[Math.floor(Math.random()*(c.length))];
                return zim.Pick.choose(val); // recursive
            } else if (c.constructor === {}.constructor) {
                if (!zot(c.noPick)) return c.noPick; // a passthrough for arrays and functions
                if (zot(c.max)) return c;
                if (zot(c.integer)) c.integer = false;
                var val = zim.Pick.rand(c.min, c.max, c.integer, c.negative);
                return val; // this is just a number in a range - no need for recursive
            } else if (typeof c == "function") {
                return zim.Pick.choose((c)()); // recursive
            }
            return obj;
        } else {
            return obj;
        }
	}//-17.6

	// DOM CODE

// SUBSECTION HTML FUNCTIONS

/*--
zim.scrollX = function(num, time)

scrollX
zim function

DESCRIPTION
This function gets or sets how many pixels from the left the browser window has been scrolled.
If num is provided then the function scrolls the window to this x position.
If num and time are provided it animates the window to the x position in time milliseconds.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// hide the logo if the page is scrolled left more than 200 pixels
if (scrollX < -200) zss("logo").display = "none";
END EXAMPLE

PARAMETERS
num - (default null) optional scroll position to go to (probably negative)
time - (default 0) time in milliseconds to take to go to the num position

RETURNS a Number
--*///+18
	zim.scrollX = function(num, time) {
		z_d("18");
		return zim.abstractScroll("X", "Left", num, time);
	}//-18


/*--
zim.scrollY = function(num, time)

scrollY
zim function

DESCRIPTION
This function gets or sets how many pixels from the top the browser window has been scrolled.
If num is provided then the function scrolls the window to this y position.
If num and time are provided it animates the window to the y position in time milliseconds.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// animate the scroll position down 100 pixels in half a second
scrollY(scrollY()-100, 500);
END EXAMPLE

PARAMETERS
num - (default null) optional scroll position to go to (probably negative)
time - (default 0) time in milliseconds to take to go to the num position

RETURNS a Number
--*///+19
	zim.scrollY = function(num, time) {
		z_d("19");
		return zim.abstractScroll("Y", "Top", num, time);
	}//-19

	//+20
	zim.abstractScroll = function(dir, side, num, time) {
		z_d("20");
		var perpend = (dir == "X") ? "Y" : "X"; // perpendicular direction
		if (zot(num)) {
			var safari = 0;
			var browser=navigator.applicationName;
			var navindex=navigator.userAgent.indexOf('Safari');
			if (navindex != -1 || browser=='Safari') {
				var safari = 1;
			}
			return (document.documentElement && document.documentElement["scroll"+side]) || document.body["scroll"+side];
		} else if (zot(time)) {
			window.scrollTo(zim["scroll"+perpend](), num);
		} else {
			var interval = 50;
			if (time < interval) time = interval;
			var steps = time/interval;
			var current = zim["scroll"+dir]();
			var amount = num - current;
			var diff = amount/steps;
			var count = 0;
			var scrollInterval = setInterval(function() {
				count++;
				current+=diff;
				window.scrollTo(zim["scroll"+perpend](), current);
				if (count >= steps) {
					window.scrollTo(zim["scroll"+perpend](), num);
					clearInterval(scrollInterval);
				}
			}, interval);
		}
		return num;
	}//-20

/*--
zim.windowWidth = function()

windowWidth
zim function

DESCRIPTION
Returns the width of a window.
(window.clientWidth or window.innerWidth)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
if (windowWidth() < 500) zss("related").display = "none";
END EXAMPLE

RETURNS a Number
--*///+21
	zim.windowWidth = function() {
		z_d("21");
		return isNaN(window.innerWidth) ? window.clientWidth : window.innerWidth;
	}//-21

/*--
zim.windowHeight = function()

windowHeight
zim function

DESCRIPTION
Returns the height of a window.
(window.clientHeight or window.innerHeight)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
if (windowHeight() > 1000) zgo("big.html");
END EXAMPLE

RETURNS a Number
--*///+22
	zim.windowHeight = function() {
		z_d("22");
		return isNaN(window.innerHeight) ? window.clientHeight : window.innerHeight;
	}//-22

/*--
zim.getQueryString = function(string)

getQueryString
zim function

DESCRIPTION
Turns the HTML query string into a object.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// currentHTML page myPage.html?party=true&toys=many
var details = getQueryString();
zog(details.party); // "true"
zog(details.toys); // "many"
loop(details, function(key, val, i) {
	zog(key, val, i);
});
// outputs:
// party true 0
// toys many 1
END EXAMPLE

EXAMPLE
// an array of values is created if a query string has multiple properties with the same name:
var collection = getQueryString("type=dog&age=10&age=20&age=30");
zog(collection.age); // [10,20,30]
END EXAMPLE

PARAMETERS
string - (default null) null will get string from end of HTML page after ?
	set the key value pairs (without question mark) to parse a custom string
	eg. party=true&toys=many

RETURNS an object literal with properties matching the keys and values matching the values (or undefined if no query string)
--*///+22.5
	zim.getQueryString = function(s) {
		z_d("22.5");
		if (zot(s)) s = location.search.replace("?","");
		if (s == "") return;
		var vars = s.split("&");
		var obj = {};
		for (var i=0; i<vars.length; i++) {
			var pair = vars[i].split("=");
			if (typeof obj[pair[0]] == "undefined") {
				obj[pair[0]] = decodeURIComponent((pair[1] + '').replace(/\+/g, '%20'));
			} else if (typeof obj[pair[0]] == "string") {
				obj[pair[0]] = [obj[pair[0]], decodeURIComponent((pair[1] + '').replace(/\+/g, '%20'))];
			} else {
				obj[pair[0]].push(decodeURIComponent((pair[1] + '').replace(/\+/g, '%20')));
			}
		}
		return obj;
	}//-22.5

/*--
zim.swapHTML = function(idA, idB)

swapHTML
zim function

DESCRIPTION
Pass in two tag ids as strings and this function will swap their innerHTML content.
The content (including nested tags) will be swapped.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// exchanges the content of two divs called question and answer
swapHTML("question","answer");
END EXAMPLE

PARAMETERS
idA, idB - String names of the tag id with which to swap innerHTML values

RETURNS Boolean indicating success
--*///+17.2
	zim.swapHTML = function(idA, idB) {
		z_d("17.2");
		return zim.swapProperties("innerHTML", zid(idA), zid(idB));
	}//-17.2

/*--
zim.urlEncode = function(string)

urlEncode
zim function

DESCRIPTION
Matches PHP urlencode and urldecode functions
for passing data on end of URL.
NOTE: only encode values of key=value pairs (not keys and not both keys and values)
NOTE: JSON automatically encodes and decodes

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var motto = "good = life & life = now";
zgo("submit.php?motto="+urlEncode(motto));
END EXAMPLE

PARAMETERS
string - a value to URL encode (space to plus, etc.)

RETURNS a String
--*///+23
	zim.urlEncode = function(s) {
		z_d("23");
		var s = (s + '').toString();
		return encodeURIComponent(s).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').
		replace(/\)/g, '%29').replace(/\*/g, '%2A').replace(/%20/g, '+');
	}//-23

/*--
zim.urlDecode = function(string)

urlDecode
zim function

DESCRIPTION
Matches PHP urlencode and urldecode functions
for receiving raw data from a source that URLencodes.
NOTE: JSON automatically encodes and decodes

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var pairs = command.split("&");
var motto = urlDecode(pairs[0].split("=")[1]);
END EXAMPLE

PARAMETERS
string - a URLencoded String to decode

RETURNS a String
--*///+24
	zim.urlDecode = function(s) {
		z_d("24");
		 return decodeURIComponent((s + '').replace(/\+/g, '%20'));
	}//-24

/*--
zim.setCookie = function(name, value, days)

setCookie
zim function

DESCRIPTION
Sets an HTML cookie to remember some user data your site has set over time.
If no days, it will be a session cookie (while browser is open).

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var visits = getCookie("visits");
if (zot(visits)) visits = 0;
setCookie("visits", ++visits);
END EXAMPLE

PARAMETERS
name - a String name for your cookie
value - a String value that you want to store
days - (default 0) for how many days do you want to store the cookie

ALSO: see getCookie and deleteCookie

RETURNS a Boolean indicating success
--*///+25
	zim.setCookie = function(name, value, days) {
		z_d("25");
		if (zot(name) || zot(value)) return;
		if (days) {
			var date = new Date();
			date.setTime(date.getTime()+(days*24*60*60*1000));
			var expires = "; expires="+date.toGMTString();
		} else {
			var expires = "";
		}
		document.cookie = name+"="+escape(value)+expires+"; path=/";
		return true;
	}//-25

/*--
zim.getCookie = function(name)

getCookie
zim function

DESCRIPTION
Gets an HTML cookie that you have previously set.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var visits = getCookie("visits");
if (zot(visits)) visits = 0;
setCookie("visits", ++visits);
END EXAMPLE

PARAMETERS
name - the String name of your stored cookie

ALSO: see setCookie and deleteCookie

RETURNS a String or undefined if not found
--*///+26
	zim.getCookie = function(name) {
		z_d("26");
		var outer = document.cookie.split(/;\s*/);
		var cookies = new Array();
		var inner;
		for (i=0; i<outer.length; i++) {
			inner = outer[i].split("=");
			cookies[inner[0]] = inner[1];
		}
		if (typeof cookies[name] == 'undefined') return undefined;
		return unescape(cookies[name]);
	}//-26

/*--
zim.deleteCookie = function(name)

deleteCookie
zim function

DESCRIPTION
Deletes an HTML cookie.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
deleteCookie("visits"); // clears the cookie
END EXAMPLE

PARAMETERS
name - the String name of your stored cookie to delete

ALSO: see setCookie and getCookie

RETURNS a Boolean indicating success
--*///+27
	zim.deleteCookie = function(name) {
		z_d("27");
		if (zot(zim.getCookie(name))) return false;
		zim.setCookie(name,"",-1);
		return true;
	}//-27


if (typeof(createjs) == "undefined") {if (zon) {zog("ZIM >= 4.3.0 requires createjs namespace to be loaded (import createjs before zim)");} return zim;}



////////////////  ZIM DISPLAY  //////////////

// Zim Display (formerly Zim Build) adds common display classes for multies (interactive media)
// classes in this module require createjs namespace to exist and in particular easel.js
// available at http://createjs.com

/*--
zim.Coordinates = function(canvasID)

Helper functions for localToGlobal, globalToLocal and localToLocal

--*///+50.43
zim.coordinatesCheck = false;
function localToGlobal(x,y,scope,func) {
	if (!zim.coordinatesCheck) {z_d("50.43"); zim.coordinatesCheck=true;}
	if (x==null || y==null) return;
	var point = func.call(scope,x,y);
	var stage = scope.stage;
	if (typeof zimDefaultFrame == "undefined") {
		if (!stage) return point;
		zimDefaultFrame = {stage:stage, canvas:stage.canvas};
	}
	if (!stage) stage = zimDefaultFrame.stage;
	// if (scope == stage) return point;
	point.x /= stage&&stage.scaleX?stage.scaleX:1;
	point.y /= stage&&stage.scaleY?stage.scaleY:1;
	return point;
}
function globalToLocal(x,y,scope,func) {
	if (!zim.coordinatesCheck) {z_d("50.43"); zim.coordinatesCheck=true;}
	if (x==null || y==null) return;
	var stage = scope.stage;
	if (typeof zimDefaultFrame == "undefined") {
		if (!stage) return func.call(scope,x,y);
		zimDefaultFrame = {stage:stage, canvas:stage.canvas};
	}
	if (!stage) stage = zimDefaultFrame.stage;
	// if (scope == stage) return func.call(scope,x,y);
	x *= stage.scaleX;
	y *= stage.scaleY;
	var point = func.call(scope,x,y);
	return point;
}
function localToLocal(x,y,target,scope) {
	if (!zim.coordinatesCheck) {z_d("50.43"); zim.coordinatesCheck=true;}
	if (x==null || y==null || target==null) return;
	var point = scope.localToGlobal(x,y);
	var stage = scope.stage;
	if (typeof zimDefaultFrame == "undefined") {
		if (!stage) return target.globalToLocal(point.x, point.y);
		zimDefaultFrame = {stage:stage, canvas:stage.canvas};
	}
	if (!stage) stage = zimDefaultFrame.stage;
	if (target == stage) return point;
	if (point) return target.globalToLocal(point.x, point.y);
}
//-50.43

/*--
zim.Stage = function(canvasID)

Stage
zim class - extends a createjs.Stage which extends a createjs.Container

DESCRIPTION
An extension of a createjs.Stage that includes read only type, width and height properties, loop and hitTestGrid methods.
When using zim.Frame, there should be no reason to make a zim.Stage.
This was put in place to match the ZIM TypeScript typings for stage width and height.
Also see https://www.createjs.com/docs/easeljs/classes/Stage.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var stage = new Stage("canvasID");
END EXAMPLE

PARAMETERS
canvasID - (default null) string ID for canvas tag

METHODS
loop(call, reverse, step, start, end) - see the ZIM Display Methods loop() for details
	 see the ZIM Display Methods loop() for details
hitTestGrid(width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type)
	see the ZIM Display Methods hitTestGrid() for details
See the CreateJS Easel Docs for Stage methods, such as:
clear, update, toDataURL
And all the Container methods such as:
on, off, setBounds, getBounds, globalToLocal, etc.

PROPERTIES
type - holds the class name as a String
width - read only width set by ZIM Frame
height - read only height set by ZIM Frame

ALSO: See the CreateJS Easel Docs for Stage properties, such as:
autoClear, canvas, nextStage, etc.
and all the Container properties, such as:
children, mouseChildren, filters, cacheCanvas, etc.

EVENTS
See the CreateJS Easel Docs for Stage events, such as:
mouseenter, mouseleave, stagemousedown, stagemousemove, stagemouseup, drawstart, drawend, etc.
and all the Container events, such as:
click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+50.44
zim.Stage = function(canvasID) {
		z_d("50.44");
		if (zot(canvasID)) return;
		var tag = canvasID.tagName=="CANVAS"?canvasID:zid(canvasID);
		if (zot(tag)) return;
		this.cjsStage_constructor(canvasID);
		this.setBounds(0,0,tag.width,tag.height);
		this.type = "Stage";
		this.cache = function(a,b,c,d,scale,options) {
			if (zot(c)) {
				if (zot(a)) {
					var bounds = this.getBounds();
					if (!zot(bounds)) {
						var added = this.borderWidth > 0 ? this.borderWidth/2 : 0;
						a = bounds.x-added;
						b = bounds.y-added;
						c = bounds.width+added*2;
						d = bounds.height+added*2;
					}
				} else {
					c = a;
					d = b;
					a = 0;
					b = 0;
				}
			}
			this.cjsStage_cache(a,b,c,d,scale,options);
			return this;
		}
		this.loop = function(call, reverse, step, start, end) {
			return zim.loop(this, call, reverse, step, start, end);
		}
		this.hitTestGrid = function(width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type) {
			return zim.hitTestGrid(this, width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type);
		}
		this.localToGlobal = function(x,y) {
			return localToGlobal(x,y,this,this.cjsStage_localToGlobal);
		}
		this.globalToLocal = function(x,y) {
			return globalToLocal(x,y,this,this.cjsStage_globalToLocal);
		}
		this.localToLocal = function(x,y,target) {
			return localToLocal(x,y,target,this);
		}
	}
	zim.extend(zim.Stage, createjs.Stage, ["cache","localToLocal","localToGlobal","globalToLocal"], "cjsStage", false);

	//-50.44

/*--
zim.StageGL = function(canvasID, options)

StageGL
zim class - extends a zim.Stage which extends a createjs.Stage

DESCRIPTION
An extension of a zim.Stage for WebGL support
See ZIM Stage and https://www.createjs.com/docs/easeljs/classes/StageGL.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var stage = new StageGL("canvasID", {preserveBuffer:true, antialias:true});
END EXAMPLE

PARAMETERS
canvasID - (default null) string ID for canvas tag
options - (default null) an object literal with the following properties
	FROM https://www.createjs.com/docs/easeljs/classes/StageGL.html
    preserveBuffer (default false)
    	If true, the canvas is NOT auto-cleared by WebGL (the spec discourages setting this to true). This is useful if you want persistent draw effects.
	antialias (default false)
		Specifies whether or not the browser's WebGL implementation should try to perform anti-aliasing. This will also enable linear pixel sampling on power-of-two textures (smoother images).
	transparent (default false)
		If true, the canvas is transparent. This is very expensive, and should be used with caution.
	premultiply (default false)
		Alters color handling. If true, this assumes the shader must account for pre-multiplied alpha. This can help avoid visual halo effects with some assets, but may also cause problems with other assets.
	autoPurge (default 1200)
	 	How often the system should automatically dump unused textures with purgeTextures(autoPurge) every autoPurge/2 draws. See purgeTextures for more information.

METHODS
loop(call, reverse, step, start, end) - see the ZIM Display Methods loop() for details
	 see the ZIM Display Methods loop() for details
hitTestGrid(width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type)
	see the ZIM Display Methods hitTestGrid() for details
See the CreateJS Easel Docs for StageGL methods:
https://www.createjs.com/docs/easeljs/classes/StageGL.html

PROPERTIES
type - holds the class name as a String
width - read only width set by ZIM Frame
height - read only height set by ZIM Frame

See the CreateJS Easel Docs for Stage properties:
https://www.createjs.com/docs/easeljs/classes/StageGL.html

EVENTS
See the CreateJS Easel Docs for StageGL events, such as:
mouseenter, mouseleave, stagemousedown, stagemousemove, stagemouseup, drawstart, drawend, etc.
and all the Container events, such as:
click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+50.45
zim.StageGL = function(canvasID, options) {
		z_d("50.45");
		if (zot(canvasID)) return;
		var tag = canvasID.tagName=="CANVAS"?canvasID:zid(canvasID);
		if (zot(tag)) return;
		this.cjsStageGL_constructor(canvasID, options);
		this.setBounds(0,0,tag.width,tag.height);
		this.type = "StageGL";
		this.cache = function(a,b,c,d,scale,options) {
			if (zot(c)) {
				if (zot(a)) {
					var bounds = this.getBounds();
					if (!zot(bounds)) {
						var added = this.borderWidth > 0 ? this.borderWidth/2 : 0;
						a = bounds.x-added;
						b = bounds.y-added;
						c = bounds.width+added*2;
						d = bounds.height+added*2;
					}
				} else {
					c = a;
					d = b;
					a = 0;
					b = 0;
				}
			}
			this.cjsStageGL_cache(a,b,c,d,scale,options);
			return this;
		}
		this.loop = function(call, reverse, step, start, end) {
			return zim.loop(this, call, reverse, step, start, end);
		}
		this.hitTestGrid = function(width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type) {
			return zim.hitTestGrid(this, width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type);
		}
		this.localToGlobal = function(x,y) {
			return localToGlobal(x,y,this,this.cjsStageGL_localToGlobal);
		}
		this.globalToLocal = function(x,y) {
			return globalToLocal(x,y,this,this.cjsStageGL_globalToLocal);
		}
		this.localToLocal = function(x,y,target) {
			return localToLocal(x,y,target,this);
		}
	}
	zim.extend(zim.StageGL, createjs.StageGL, ["cache","localToLocal","localToGlobal","globalToLocal"], "cjsStageGL", false);

	//-50.45

/*--
zim.Container = function(a, b, c, d, style, group, inherit)

Container
zim class - extends a createjs.Container

DESCRIPTION
A Container object is used to hold other display objects or other containers.
You can then move or scale the container and all objects inside will move or scale.
You can apply an event on a container and use the target property of the event object
to access the object in the container that caused the event
or use the currentTarget property of the event object to access the container itself.
Containers do not have bounds unless some items in the container have bounds -
at which point the bounds are the combination of the bounds of the objects with bounds.
You can manually set the bounds with setBounds(x,y,w,h) - read the CreateJS docs.
Or pass in width and height, or boundsX, boundsY, width, height to have Container set bounds
Manually set bounds will not update automatically unless you setBounds(null).

NOTE: All the ZIM shapes and components extend the Container.
This means all shapes and components inherit the methods and properties below
and indeed, the Container inherits all the createjs.Container methods and properties.
See the CreateJS documentation for x, y, alpha, rotation, on(), addChild(), etc.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var container = new Container();
stage.addChild(container);
container.x = 100; container.y = 100;

// demonstration of adding drag() to a Container
var rect = new Rectangle(100, 100, "blue");
container.addChild(rect); // add rectangle to container
var circle = new Circle(40, "red");
circle.center(container) // add the circle to the container and center
container.drag(); // will drag either the rectangle or the circle
container.drag({currentTarget:true}); // will drag both the rectangle and the circle

// below will reduce the alpha of the object in the container that was clicked (target)
container.on("click" function(e) {e.target.alpha = .5; stage.update();})
// below will reduce the alpha of all the objects in the container (currentTarget)
container.on("click" function(e) {e.currentTarget.alpha = .5; stage.update();})
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
** Container supports three different sets of parameters as follows:
a - (default null) - width and height equal to parameter a (x and y will be 0)
a, b - (default null) - the width and height (x and y will be 0)
a, b, c, d - (default null) - the x, y, width and height of the bounds
	if parameter a is not set, then the Container will take bounds that grow with its content
	the bounds of the Container can be set at any time with setBounds(a, b, c, d)
	if the bounds are set, then the Container bounds will not change as content is added
	the bounds can be removed with setBounds(null) and the Container will get auto bounds
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly


NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle();
circle.center(stage); // add circle to stage and center
circle.drag();

// alternatively, we can still use the traditional ZIM functions:
center(circle, stage);
drag(circle);

// ZIM DUO works the same way as before - eg.
circle.drag({slide:true});
END EXAMPLE

METHODS
* This class has all the DISPLAY METHODS introduced in ZIM 4TH
* the methods are available to all ZIM Display objects that extend a ZIM Container
* such as ZIM Rectangle, Circle, Triangle, BLob
* as well as all components like: Label, Button, Slider, Dial, Tab, Pane, etc.
* as well as the ZIM display wrappers: Container, Shape, Sprite, MovieClip and Bitmap
cache(width||x, height||y, null||width, null||height, scale, options) - overrides CreateJS cache() and returns object for chaining
	If you do not provide the first four parameters, then the cache dimensions will be set to the bounds of the object
	width||x - (default getBounds().x) the width of the chache - or the x if first four parameters are provided
	height||y - (default getBounds().y) the height of the chache - or the y if first four parameters are provided
	width - (default getBounds().width) the width of the chache - or null if first two parameters are provided
	height - (default getBounds().height) the height of the chache - or null if first two parameters are provided
	scale - (default 1) set to 2 to cache with twice the fidelity if later scaling up
	options - (default null) additional parameters for cache logic - see CreateJS somewhere for details
setBounds(width||x, height||y, null||width, null||height) - overrides CreateJS setBounds() and returns object for chaining
	If you do not provide the any parameters, then the bounds will be reset to the calculated bounds
	width||x - (default null) the width of the bounds - or the x if four parameters are provided
	height||y - (default width) the height of the bounds - or the y if four parameters are provided
	width - (default null) the width of the bounds - or null if only the first two parameters are provided
	height - (default null) the height of the bounds - or null if only the first two parameters are provided
hasProp(property as String) - returns true if property exists on object else returns false
clone() - clones the container, its properties and all its children
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
** bounds must be set first (or width and height parameters set) for these to work
** setting these adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scale, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+50.5

	zim.containerCheck = false;
	zim.Container = function(a, b, c, d, style, group, inherit) {
		var sig = "a, b, c, d, style, group, inherit";
		var duo; if (duo = zob(zim.Container, arguments, sig, this)) return duo;
		if (!zim.containerCheck) {z_d("50.5"); zim.containerCheck=true;}
		this.cjsContainer_constructor();
		this.type = "Container";
		this.group = group;
		var that = this;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(a)) a = DS.a!=null?DS.a:null;
		if (zot(b)) b = DS.b!=null?DS.b:null;
		if (zot(c)) c = DS.c!=null?DS.c:null;
		if (zot(d)) d = DS.d!=null?DS.d:null;

		var n = normalizeBounds(a, b, c, d);
		function normalizeBounds(a, b, c, d) {
			var bounds = [];
			if (zot(a)) {
				bounds = [a,b,c,d];
			} else if (!zot(c)) {
				bounds[0] = a;
				bounds[2] = c;
				bounds[1] = b;
				bounds[3] = d;
			} else {
				bounds[0] = 0;
				bounds[2] = a;
				bounds[1] = 0;
				bounds[3] = b;
			}
			if (zot(bounds[3])) bounds[3] = bounds[2];
			return bounds;
		}
		if (!zot(a)) this.setBounds(n[0],n[1],n[2],n[3]); // es6 to fix

		this.cache = function(a,b,c,d,scale,options) {
			var bounds = this.getBounds();
			if (zot(c)) {
				if (zot(a)) {
					if (!zot(bounds)) {
						var added = this.borderWidth > 0 ? this.borderWidth/2 : 0;
						a = bounds.x-added;
						b = bounds.y-added;
						c = bounds.width+added*2;
						d = bounds.height+added*2;
					}
				} else {
					c = a;
					d = b;
					a = 0;
					b = 0;
				}
			}
			if (this.type == "Triangle") {
				a-=this.borderWidth?this.borderWidth:0;
				c+=this.borderWidth?this.borderWidth*2:0;
				b-=this.borderWidth?this.borderWidth:0;
				d+=this.borderWidth?this.borderWidth*2:0;
			}
			this.cjsContainer_cache(a,b,c,d,scale,options);
			if (bounds) this.setBounds(bounds.x, bounds.y, bounds.width, bounds.height);
			return this;
		}
		this.localToGlobal = function(x,y) {
			return localToGlobal(x,y,this,this.cjsContainer_localToGlobal);
		}
		this.globalToLocal = function(x,y) {
			return globalToLocal(x,y,this,this.cjsContainer_globalToLocal);
		}
		this.localToLocal = function(x,y,target) {
			return localToLocal(x,y,target,this);
		}
		this.setBounds = function(a,b,c,d) {
			var n = normalizeBounds(a, b, c, d);
			this.cjsContainer_setBounds(n[0],n[1],n[2],n[3]);
			return this;
		}
		if (style!==false) zimStyleTransforms(this, DS); // global function - would have put on DisplayObject if had access to it
		this.clone = function() {
			var currentBounds = this.getBounds();
			if (zot(currentBounds)) currentBounds = {x:null, y:null, width:null, height:null};
			return this.cloneChildren(this.cloneProps(new zim.Container(currentBounds.x,currentBounds.y,currentBounds.width,currentBounds.height, style, this.group, inherit)));
		}
		this.hasProp = function(prop) {
			return (!zot(this[prop]) || this.hasOwnProperty(prop))
		}
	}
	zim.Container.prototype.dispose = function() {
		recursiveDispose(this);
		return true;
	}
	function recursiveDispose(obj) {
		obj.removeAllEventListeners();
		if (obj.numChildren) {
			for (var i=obj.numChildren-1; i>=0; i--) {
				recursiveDispose(obj.getChildAt(i));
			}
		}
		if (obj.parent) obj.parent.removeChild(obj);
	}
	zimify(zim.Container.prototype);
	zim.extend(zim.Container, createjs.Container, ["cache","setBounds","clone","localToLocal","localToGlobal","globalToLocal"], "cjsContainer", false);
	//-50.5

/*--
zim.Shape = function(a, b, c, d, graphics, style, group, inherit)

Shape
zim class - extends a createjs.Shape

DESCRIPTION
ZIM Shape lets you draw dynamic shapes beyond the ZIM provided shapes.
You make a new shape object and then draw in its graphics property
using similar commands to the HTML Canvas commands (and Flash Bitmap drawing).
See the CreateJS Easel Shapes and Graphics docs:
http://www.createjs.com/docs/easeljs/classes/Graphics.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var shape = new Shape().addTo();
shape.graphics.beginFill(red).drawRect(0,0,200,100);
// similar to Rectangle(200, 100, "Red");

// we can draw lines, etc.
var g = shape.graphics; // shorter reference to graphics object
g.beginStroke(blue).moveTo(200,200).lineTo(300,300);

// we can continue to draw as much as we want in the same shape
// there is also a tiny API with shortcuts: stroke, fill, etc.
g.s(purple).ss(5).f(blue).mt(400,400).qt(500,300,600,400);
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
** Shape supports three different sets of parameters as follows:
a - (default null) - width and height equal to parameter a (x and y will be 0)
a, b - (default null) - the width and height (x and y will be 0)
a, b, c, d - (default null) - the x, y, width and height of the bounds
graphics - (default null) a CreateJS Graphics instance (see CreateJS docs)
	or just use the graphics property of the shape object (like usual)
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
cache(width||x, height||y, null||width, null||height, scale, options) - overrides CreateJS cache() and returns object for chaining
	If you do not provide the first four parameters, then the cache dimensions will be set to the bounds of the object
	width||x - (default getBounds().x) the width of the chache - or the x if first four parameters are provided
	height||y - (default getBounds().y) the height of the chache - or the y if first four parameters are provided
	width - (default getBounds().width) the width of the chache - or null if first two parameters are provided
	height - (default getBounds().height) the height of the chache - or null if first two parameters are provided
	scale - (default 1) set to 2 to cache with twice the fidelity if later scaling up
	options - (default null) additional parameters for cache logic - see CreateJS somewhere for details
setBounds(width||x, height||y, null||width, null||height) - overrides CreateJS setBounds() and returns object for chaining
	width||x - (default null) the width of the bounds - or the x if four parameters are provided
	height||y - (default width) the height of the bounds - or the y if four parameters are provided
	width - (default null) the width of the bounds - or null if only the first two parameters are provided
	height - (default null) the height of the bounds - or null if only the first two parameters are provided
hasProp(property) - returns true if String property exists on object else returns false
clone(recursive) - makes a copy of the shape
	recursive defaults to true so copy will have own copy of graphics
	set recursive to false to have clone share graphic property
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), placeReg(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
** bounds must be set first (or width and height parameters set) for these to work
** setting these adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Shape properties, such as:
graphics, x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseEnabled, etc.

EVENTS
See the CreateJS Easel Docs for Shape events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+50.6
	zim.Shape = function(a, b, c, d, graphics, style, group, inherit) {
		var sig = "a, b, c, d, graphics, style, group, inherit";
		var duo; if (duo = zob(zim.Shape, arguments, sig, this)) return duo;
		z_d("50.6");
		this.cjsShape_constructor(graphics);
		this.type = "Shape";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);
		var that = this;

		if (zot(a)) a = DS.a!=null?DS.a:null;
		if (zot(b)) b = DS.b!=null?DS.b:null;
		if (zot(c)) c = DS.c!=null?DS.c:null;
		if (zot(d)) d = DS.d!=null?DS.d:null;

		var n = normalizeBounds(a, b, c, d);
		function normalizeBounds(a, b, c, d) {
			var bounds = [];
			if (!zot(c)) {
				bounds[0] = a;
				bounds[2] = c;
				bounds[1] = b;
				bounds[3] = d;
			} else {
				bounds[0] = 0;
				bounds[2] = a;
				bounds[1] = 0;
				bounds[3] = b;
			}
			if (zot(bounds[3])) bounds[3] = bounds[2];
			return bounds;
		}
		if (!zot(a)) this.setBounds(n[0],n[1],n[2],n[3]); // es6 to fix

		this.cache = function(a,b,c,d,scale,options) {
			if (zot(c)) {
				if (zot(a)) {
					var bounds = this.getBounds();
					if (!zot(bounds)) {
						var added = this.borderWidth > 0 ? this.borderWidth/2 : 0;
						a = bounds.x-added;
						b = bounds.y-added;
						c = bounds.width+added*2;
						d = bounds.height+added*2;
					}
				} else {
					c = a;
					d = b;
					a = 0;
					b = 0;
				}
			}
			this.cjsShape_cache(a,b,c,d,scale,options);
			return this;
		}
		this.setBounds = function(a,b,c,d) {
			var n = normalizeBounds(a, b, c, d);
			this.cjsShape_setBounds(n[0],n[1],n[2],n[3]);
			return this;
		}
		if (style!==false) zimStyleTransforms(this, DS); // global function - would have put on DisplayObject if had access to it
		this.clone = function(recursive) {
			if (zot(recursive)) recursive = true;
			var currentBounds = this.getBounds();
			if (zot(currentBounds)) currentBounds = {x:null, y:null, width:null, height:null};
			var c = that.cloneProps(new zim.Shape(currentBounds.x,currentBounds.y,currentBounds.width,currentBounds.height, graphics, style, group, inherit));
			if (recursive) c.graphics = that.graphics.clone();
			else c.graphics = that.graphics;
			return c;
		}
		this.localToGlobal = function(x,y) {
			return localToGlobal(x,y,this,this.cjsShape_localToGlobal);
		}
		this.globalToLocal = function(x,y) {
			return globalToLocal(x,y,this,this.cjsShape_globalToLocal);
		}
		this.localToLocal = function(x,y,target) {
			return localToLocal(x,y,target,this);
		}
		this.hasProp = function(prop) {
			return (!zot(this[prop]) || this.hasOwnProperty(prop))
		}
		this.dispose = function() {
			this.graphics.c();
			this.removeAllEventListeners();
			if (this.parent) this.parent.removeChild(this);
		}
	}
	zim.extend(zim.Shape, createjs.Shape, ["cache","clone","setBounds","localToLocal","localToGlobal","globalToLocal"], "cjsShape", false);
	zimify(zim.Shape.prototype);
	//-50.6



/*--
zim.Bitmap = function(image, width, height, id, style, group, inherit)

Bitmap
zim class - extends a createjs.Bitmap

DESCRIPTION
Makes a Bitmap object from an image.
It is best to use the assets and path parameters of ZIM Frame or the loadAssets() method of Frame
to preload the image and then use the asset() method to access the Bitmap.
See the ZIM Frame class and asset example on the ZIM Frame page of templates.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var frame = new Frame();
frame.on("ready", function() {
	var stage = frame.stage;
	frame.loadAssets("logo.jpg");
	frame.on("complete", function() {
		var logo = frame.asset("logo.jpg"); // logo is a Bitmap
		logo.center();
		stage.update();
	});
});
END EXAMPLE

EXAMPLE
// fill a Bitmap with noise:
var noise = new Noise();
// empty Bitmap size 200, 200
var bmp = new Bitmap(null,200,200).center(stage);
// we fill the bitmap starting from top left going across in the inner loop,
// then down, then across, etc. until we get to bottom right.
var f = 50; // used to make noise bigger or smaller - see the blob comment below
for (var y = 0; y < bmp.height; y++) {
	for (var x = 0; x < bmp.width; x++) {
		// the noise methods return a number from -1 to 1
		// by adding 1 we get a number between 0 and 2 then divide by 2
		// and we multiply this by 255 to get a number between 0 and 255
		value = (noise.simplex2D(x, y)+1)/2 * 255;
		// or get blobs by smoothing and adjusting frequency:
		// var value = smoothStep((noise.simplex2D(x/f, y/f)+1)/2, .3,.35) * 255;
		// imageData is four values per pixel
		// the red, green, blue and alpha
		// in one big long array - each value will be constrained to between 0 and 255
		// this i value will increase by 4 each time
		// then we write the same value for red, green, blue to get a shade of grey
		var i = (x + y * bmp.width) * 4;
		bmp.imageData.data[i] = value; // red (0-255)
		bmp.imageData.data[i + 1] = value; // green (0-255)
		bmp.imageData.data[i + 2] = value; // blue (0-255)
		bmp.imageData.data[i + 3] = 255; // alpha (0-255)
	}
}
bmp.drawImageData();
END EXAMPLE

EXAMPLE
// applying filters
var bitmap = frame.asset("statue.jpg");
bitmap.filters = [new createjs.BlurFilter(25, 25, 1)];
bitmap.cache().center();
END EXAMPLE

EXAMPLE
// getting the color at point(100, 100) on the Bitmap
var bitmap = frame.asset("statue.jpg").cache();
var ctx = bitmap.cacheCanvas.getContext('2d');
var data = ctx.getImageData(100, 100, 1, 1).data;
var color = "rgba("+data.join(", ")+")";
END EXAMPLE

EXAMPLE
// a Base64 image:
var image = "data:image/png;base64,longlistofcharacters";
var logo;
Bitmap.fromData(image, function (bitmap) {
	logo = bitmap.center();
	stage.update();
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
image - an HTML image URL (may not load right away - see Frame loadAssets)
width - (default 100) used with putImageData to draw a Bitmap otherwise ignored
height - (default 100) used with putImageData to draw a Bitmap otherwise ignored
id - an optional id
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
fromData(data, callback) - STATIC method so use the Bitmap class directly: Bitmap.fromData()
	The callback will receive a reference to the Bitmap after 50ms or 100ms.
	There is no event for making a Bitmap from base64 for instance - so this will have to do.
drawImageData(x, y, sourceX, srcY, srcWidth, srcHeight) - draws the Bitmap's imageData data to the Bitmap
	NOTE: This is only used when dynamically drawing a Bitmap with data - not for your normal picture
	See the imageData property which should be set before using the drawImageData() method
	ZIM calls a putImageData method for the HTML Canvas and then transfers this to the Bitmap
	See also https://www.w3schools.com/tags/canvas_putimagedata.asp - but let ZIM do the work...
cache(width||x, height||y, null||width, null||height, scale, options) - overrides CreateJS cache() and returns object for chaining
	** Usually you do not want to cache a Bitmap as it is already a Bitmap ;-)
	** But for applying a filter or using a cacheCanvas to get a context, etc. then you might.
	If you do not provide the first four parameters, then the cache dimensions will be set to the bounds of the object
	width||x - (default getBounds().x) the width of the chache - or the x if first four parameters are provided
	height||y - (default getBounds().y) the height of the chache - or the y if first four parameters are provided
	width - (default getBounds().width) the width of the chache - or null if first two parameters are provided
	height - (default getBounds().height) the height of the chache - or null if first two parameters are provided
	scale - (default 1) set to 2 to cache with twice the fidelity if later scaling up
	options - (default null) additional parameters for cache logic - see CreateJS somewhere for details
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Bitmap methods, such as:
on(), off(), getBounds(), setBounds(), dispatchEvent(), etc.

PROPERTIES
type - holds the class name as a String
imageData - data for the pixels stored in a data property of an ImageData object
	NOTE: This is only used when dynamically drawing a Bitmap with data - not for your normal picture
	The data property is an one dimensional Array with consecutive red, green, blue, alpha values (0-255) for each pixels
	eg. 0,0,0,255,255,255,255,255 is a white pixel with 1 alpha and a black pixel with 1 alpha
	You set this before calling the Bitmap drawImageData() method
 	See also https://developer.mozilla.org/en-US/docs/Web/API/ImageData - but let ZIM do the work
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
id (fileID in TypeScript) - the filename used in the frame.loadAssets()
	if you add the path the file name then it will be included with the id
	if you add the path with the path parameter, it will not be included with the id
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Bitmap properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseEnabled, etc.

EVENTS
See the CreateJS Easel Docs for Bitmap events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+50.7
	zim.Bitmap = function(image, width, height, id, style, group, inherit) {
		var sig = "image, width, height, id, style, group, inherit";
		var duo; if (duo = zob(zim.Bitmap, arguments, sig, this)) return duo;
		z_d("50.7");
		this.cjsBitmap_constructor(image);
		var that = this;
		this.type = "Bitmap";
		this.group = group;
	    var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		this.id = that.fileID = DS.id!=null?DS.id:id;
		if (zot(width)) width = DS.width!=null?DS.width:null;
		if (zot(height)) height = DS.height!=null?DS.height:null;
		if (!zot(width) && !zot(height)) that.setBounds(0,0,width,height);
		if (zot(width)) width = 100;
		if (zot(height)) height = 100;
		if (zimDefaultFrame) {
			// not supported by IE - thanks Chris Spolton for the find and suggested fix
			if (zimDefaultFrame.canvas.getContext("2d")) {
				this.imageData = zimDefaultFrame.canvas.getContext("2d").createImageData(width, height);
			} else {
				this.imageData = document.createElement('canvas').getContext("2d").createImageData(width, height);
				// if (ImageData) this.imageData = new ImageData(width, height);
			}
			this.drawImageData = function(x, y, sourceX, sourceY, sourceWidth, sorceHeight) {
				if (zot(x)) x = 0;
				if (zot(y)) y = 0;
				if (zot(sourceX)) sourceX = 0;
				if (zot(sourceY)) sourceY = 0;
				if (zot(sourceWidth)) sourceWidth = width;
				if (zot(sorceHeight)) sorceHeight = height;
				if (!that.proxyCanvas) {
					var c = that.proxyCanvas = document.createElement("canvas");
					c.setAttribute("width", width);
					c.setAttribute("height", height);
					that.proxyContext = c.getContext('2d');
					image = that.image = c;
				}
				if (that.proxyContext) {
					that.proxyContext.putImageData(that.imageData, x, y, sourceX, sourceY, sourceWidth, sorceHeight);
				}
			}
			if (zot(image)) that.drawImageData();

			// handle delay when creating Bitmap from data
			if (image.match && image.match(/data:image/i)) setTimeout(function() {
				if (that.stage) that.stage.update();
				setTimeout(function() {
					if (that.stage) that.stage.update();
				}, 50);
			}, 50);
		}

		this.cache = function(a,b,c,d,scale,options) {
			if (zot(c)) {
				if (zot(a)) {
					var bounds = this.getBounds();
					if (!zot(bounds)) {
						var added = this.borderWidth > 0 ? this.borderWidth/2 : 0;
						a = bounds.x-added;
						b = bounds.y-added;
						c = bounds.width+added*2;
						d = bounds.height+added*2;
					}
				} else {
					c = a;
					d = b;
					a = 0;
					b = 0;
				}
			}
			var bounds = this.getBounds();
			this.cjsBitmap_cache(a,b,c,d,scale,options);
			this.setBounds(bounds.x, bounds.y, bounds.width, bounds.height);
			return this;
		}

		if (style!==false) zimStyleTransforms(this, DS); // global function - would have put on DisplayObject if had access to it
		this.clone = function() {
			return this.cloneProps(new zim.Bitmap(image, null, null, that.fileID, style, this.group, inherit));
		}
		this.localToGlobal = function(x,y) {
			return localToGlobal(x,y,this,this.cjsBitmap_localToGlobal);
		}
		this.globalToLocal = function(x,y) {
			return globalToLocal(x,y,this,this.cjsBitmap_globalToLocal);
		}
		this.localToLocal = function(x,y,target) {
			return localToLocal(x,y,target,this);
		}
		this.hasProp = function(prop) {
			return (!zot(this[prop]) || this.hasOwnProperty(prop))
		}
		this.dispose = function() {
			this.removeAllEventListeners();
			if (this.parent) this.parent.removeChild(this);
		}
	}
	zim.Bitmap.fromData = function(data, callBack) {
		var bitmap = new zim.Bitmap(data);
		setTimeout(function() {
			callBack(bitmap);
		}, 50);
	}
	zim.extend(zim.Bitmap, createjs.Bitmap, ["cache","clone","localToLocal","localToGlobal","globalToLocal"], "cjsBitmap", false);
	zimify(zim.Bitmap.prototype);
	//-50.7

/*--
zim.Sprite = function(image, cols, rows, count, offsetX, offsetY, spacingX, spacingY, width, height, animations, json, id, globalControl, spriteSheet, label, style, group, inherit)

Sprite
zim class - extends a createjs.Sprite

DESCRIPTION
A Sprite plays an animation of a spritesheet
which is a set of images layed out in one file.
You play the Sprite with the run() method.
This animates the Sprite over a given time
with various features like playing a labelled animation,
playing animation series,
SEE: https://zimjs.com/spritesheet/index.html
AND: https://zimjs.com/spritesheet/skateboard.html
wait, loop, rewind and call functions.
This actually runs a ZIM animation and animates the frames.

NOTE: A ZIM Sprite handles both an evenly tiled spritesheet - use cols and rows
and an un-evenly tiled spritesheet - use the json parameter.
The json can come from TexturePacker for instance exported for EaselJS/CreateJS
CreateJS Easel Sprite and SpriteSheet docs:
http://www.createjs.com/docs/easeljs/classes/Sprite.html
http://www.createjs.com/docs/easeljs/classes/SpriteSheet.html
You can optionally pass in an existing createjs.SpriteSheet as a parameter.
When you do so, all other parameters are ignored.

NOTE: You can use CreateJS gotoAndPlay(), play(), etc.
but we found the framerate could not be kept
with other animations or Ticker events running.
So we recommend using the ZIM Sprite run() method.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// inside Frame template
// boom.png is a sprite sheet found online
// It has 8 columns and 6 rows that we can visually count
// We can enter a total parameter if it does not end evenly in the grid
// A graphics editor (like Photoshop) could be used to see
// if there is an offset or spacing, etc. and enter those as parameters
// In this case, we do not need to do any of this - just enter the cols and rows

frame.on("complete", function() {
	var spriteImage = frame.asset("boom.png");

	var animation = new Sprite({
		image:spriteImage,
		cols:8,
		rows:6,
		animations:{mid:[10,20], end:[30,40]} // optional animations with labels
		// see CreateJS SpriteSheet docs for the various animation format as there are a few different ones!
	});
	animation.center(stage);
	animation.run(2000); // plays the frames of the Sprite over 2 seconds (master time)

	// OR use the label to play the frames listed in animations parameter
	animation.run(1000, "mid");

	// OR run a series of animations
	// by passing an array of label objects to the label parameter
	// these each have a time so the master time is ignored
	// they can also have any of the run() parameters
	// if you provide an array of labels, you cannot rewind the overall animation
	animation.run(null, [
		{label:"mid", time:1000},
		{label:"end", time:500, loop:true, loopCount:5, call:function(){zog("loops done");}},
		{startFrame:10, endFrame:20, time:1000}
	]);

	// OR can call a function when done
	animation.run(1000, "mid", function(){
		stage.removeChild(animation);
		stage.update();
	});

	// OR can loop the animation
	animation.run({time:2000, loop:true}); // see run() parameters for more
});
END EXAMPLE

EXAMPLE
// using Sprite as a texture atlas - or spritesheet of different images
// see: https://zimjs.com/explore/fruit.html
// load in assets and path
var frame = new Frame({assets:["fruit.png", "fruit.json"], path:"assets/"});
frame.on("ready", function() {
	new Sprite({json:frame.asset("fruit.json"), label:"apple"}).center();
	frame.stage.update();
});
END EXAMPLE

EXAMPLE
// Here is an example with CreateJS SpriteSheet data
// robot.png is a sprite sheet made by ZOE based on a Flash swf
// you can also make your own with Photoshop or Texture Packer

frame.loadAssets("robot.png");
frame.on("complete", function() {

	// using ZOE to export swf animation to spritesheet data
	// spritesheet data uses the image name, not the Bitmap itself
	var image = frame.asset("robot.png").image;
	var spriteData = {
		"framerate":24,
		"images":[image],
		"frames":[[0, 0, 256, 256, 0, -54, -10], many more - etc.],
		"animations":{}
	};
	var animation = new Sprite({json:spriteData});
	animation.center(stage);
	animation.run(2000); // note, duration alternative to framerate
});

OR
// load in data from external JSON
frame.loadAssets(["robot.json", "robot.png"]);
// ... same as before
var animation = new Sprite({json:frame.asset("robot.json")});
// ... same as before

// see CreateJS SpriteSheet docs for the format of the JSON file
// including various animation formats
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
image - the ZIM Bitmap for the spritesheet
cols - (default 1) - the columns in the spritesheet
rows - (default 1) the rows in the spritesheet
count - (default cols*rows) how many total frames in the spritesheet
offsetX - (default 0) the pixels from the left edge to the frames
offsetY - (default 0) the pixels from the top edge to the frames
spacingX - (default 0) the horizontal spacing between the frames
spacingY - (default 0) the vertical spacing between the frames
width - (default image width) the width including offset and spacing for frames
height - (default image height) the height including offset and spacing for frames
animations - (default null) an object literal of labels holding frames to play
	{label:3, another:[4,10]}
	run(1000, "label") would play frame 3 for a second
	run(1000, "another") would play frames 4 to 10 for a second
	{unordered:{frames:[1,2,3,22,23,24,"anotherLabel",5,6], next:prevLabel}}
	There are also ways to set speeds - but would recommend dividing into simple labels
	and using the label series technique available with the run() method
json - (default null) a JSON string for a CreateJS SpriteSheet
	If you pass in a json parameter, all other parameters are ignored
	NOTE: remember that JSON needs quotes around the animation properties above:
	{"label":3, "another":[4,10]}
id - (default randomly assigned) an id you can use in other animations - available as sprite.id
	use this id in other animations for pauseRun and stopRun to act on these as well
globalControl - (default true) pauseRun and stopRun will control other animations with same id
spriteSheet - (default null) pass in a CreateJS SpriteSheet to build a Sprite from that
label - (default null) pass in a label to stop on initially - to play from a label use the run({label:val}) method
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
run(time, label, call, params, wait, waitedCall, waitedParams, loop, loopCount, loopWait, loopCall, loopParams, loopWaitCall, loopWaitParams, rewind, rewindWait, rewindCall, rewindParams, rewindWaitCall, rewindWaitParams, startFrame, endFrame, tweek, id, globalControl)
	The run() method animates the Sprite over an amount of time
	Would recommend this method over the CreateJS play() and gotoAndPlay()
	methods because the framerate for these get overwritten by other stage.update() calls
	With run() you get other nice ZIM animate features as well as follows:
	Returns the object for chaining
	Can be paused with pauseAnimate(true) or unpaused with pauseAnimate(false)
	Can be stopped with stopAnimate() on the Sprite
	supports DUO - parameters or single object with properties below
	time (default 1000) - the time in milliseconds to run the animations (the master time)
	label (default null) - a label specified in the Sprite animations parameter
		if this is an array holding label objects for example:
		[{label:"run", time:1000}, {label:"stand", time:2000}]
		then the sprite will play the series with the times given and ignore the master time
		Note: if any of the series has a loop and loops forever (a loopCount of 0 or no loopCount)
		then this will be the last of the series to run
		rewind is not available on the outside series but is available on an inside series
	call - (default null) the function to call when the animation is done
	params - (default target) a single parameter for the call function (eg. use object literal or array)
	wait - (default 0) milliseconds to wait before doing animation
	waitedCall - (default null) call the function after a wait time if there is one
	waitedParams - (default null) parameters to pass to the waitedCall function
	loop - (default false) set to true to loop animation
	loopCount - (default 0) if loop is true how many times it will loop (0 is forever)
	loopWait - (default 0) milliseconds to wait before looping (post animation wait)
	loopCall - (default null) calls function after loop and loopWait (not including last loop)
	loopParams - (default target) parameters to send loopCall function
	loopWaitCall - (default null) calls function after at the start of loopWait
	loopWaitParams - (default target) parameters to send loopWaitCall function
	rewind - (default false) set to true to rewind (reverse) animation (doubles animation time) (not available on label series)
	rewindWait (default 0) milliseconds to wait in the middle of the rewind
	rewindCall (default null) calls function at middle of rewind after rewindWait
	rewindParams - (default target) parameters to send rewindCall function
	rewindWaitCall (default null) calls function at middle of rewind before rewindWait
	rewindWaitParams - (default target) parameters to send rewindCall function
	startFrame - (default null - or 0) the frame to start on - will be overridden by a label with frames
	endFrame - (default null - or totalFrames) the frame to end on - will be overridden by a label with frames
	tweek - (default 1) a factor for extra time on rewind and loops if needed
	id - (default randomly assigned) an id you can use in other animations - available as sprite.id
		use this id in other animations for pauseRun and stopRun to act on these as well
	globalControl - (default true) pauseRun and stopRun will control other animations with same id
pauseRun(state) - pause or unpause the animation (including an animation series)
	state - (default true) when true the animation is paused - set to false to unpause
	returns object for chaining
stopRun() - stop the sprite from animating
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Sprite methods, such as:
play(), gotoAndPlay(), gotoAndStop(), stop(), advance(),
on(), off(), getBounds(), setBounds(), dispatchEvent(), etc.

PROPERTIES
id - an id that you can use in other animations to also be controlled by pauseRun() and stopRun()
frame - get and set the current frame of the Sprite
normalizedFrame - if animations have CreateJS speeds applied, zim handles these by making extra frames
	for example, if a speed is given of .5 then two frames are made (min resulution is .1)
normalizedFrames - an array of total frames after being normalized - really for internal usage
totalFrames - get the total frames of the Sprite - read only
animations - the animations data with labels of frames to animate
running - is the sprite animation being run (includes both paused and unpaused) - read only
runPaused - is the sprite animation paused (also returns paused if not running) - read only
	note: this only syncs to pauseRun() and stopRun() not pauseAnimate() and stopAnimate()
	note: CreateJS has paused, etc. but use that only if running the CreateJS methods
	such as gotoAndPlay(), gotoAndStop(), play(), stop()
** bounds must be set first for these to work
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Sprite properties, such as:
currentFrame, framerate, paused, currentAnimation, currentAnimationFrame, spriteSheet,
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseEnabled, etc.

EVENTS
See the CreateJS Easel Docs for Sprite events, such as:
animationend, change, added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+50.8
	zim.Sprite = function(image, cols, rows, count, offsetX, offsetY, spacingX, spacingY, width, height, animations, json, id, globalControl, spriteSheet, label, style, group, inherit) {
		var sig = "image, cols, rows, count, offsetX, offsetY, spacingX, spacingY, width, height, animations, json, id, globalControl, spriteSheet, label, style, group, inherit";
		var duo; if (duo = zob(zim.Sprite, arguments, sig, this)) return duo;
		z_d("50.8");
		this.type = "Sprite";
		this.group = group;
	    var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		var that = this;
		var sheet;
		if (zot(json) && !zot(image)) {
			if (zot(cols)) cols = DS.cols!=null?DS.cols:1;
			if (zot(rows)) rows = DS.rows!=null?DS.rows:1;
			if (zot(count)) count = DS.count!=null?DS.count:(cols*rows);
			if (zot(offsetX)) offsetX = DS.offsetX!=null?DS.offsetX:0;
			if (zot(offsetY)) offsetY = DS.offsetY!=null?DS.offsetY:0;
			if (zot(spacingX)) spacingX = DS.spacingX!=null?DS.spacingX:0;
			if (zot(spacingY)) spacingY = DS.spacingY!=null?DS.spacingY:0;
			if (zot(width)) width = DS.width!=null?DS.width:image.width;
			if (zot(height)) height = DS.height!=null?DS.height:image.height;

			var frameW = (width-offsetX+spacingX) / cols - spacingX;
			var frameH = (height-offsetY+spacingY) / rows - spacingY;
			var frames = [];
			var num = 0;
			outer:
			for (var j=0; j<rows; j++) {
				for (var i=0; i<cols; i++) {
					if (++num > count) break outer;
					frames.push([
						offsetX + i*(frameW+spacingX),
						offsetY + j*(frameH+spacingY),
						frameW,
						frameH
					]);
				}
			}
			makeSheet(image, frames, animations);
		} else if (spriteSheet) {
			sheet = spriteSheet;
			animations = sheet.animations;
		} else if (json) {
			// even though data is in JSON, may want to create SpriteSheet from image
			// so that cors will work - so see if provided an image
			// or that the images in the JSON are available in frame.assets
			frames = json.frames;
			animations = json.animations;
			if (!zot(image)) {
				makeSheet(image, frames, animations);
			} else {
				var im = json.images?json.images[0]:null;
				if (!im.split) {
					sheet = new createjs.SpriteSheet(json);
				} else {
					var imEnd = im.split("/").pop();
					if (frame.asset(imEnd).type != "EmptyAsset") {
						makeSheet(frame.asset(imEnd), frames, animations);
					} else if (frame.asset(im).type != "EmptyAsset") {
						var imFinal = frame.asset(im);
						makeSheet(frame.asset(im), frames, animations);
					} else {
						sheet = new createjs.SpriteSheet(json);
					}
				}
			}
		} else {
			return;
		}

		function makeSheet(image, frames, animations) {
			var spriteData = {
				images:[image.image], // note, this takes the image, not the Bitmap
				frames:frames,
				animations:animations?animations:[]
			};
			sheet = new createjs.SpriteSheet(spriteData);
		}

		this.animations = animations;
		this.cjsSprite_constructor(sheet, label);
		if (!zot(label)) this.stop();

		if (zot(id)) id = zim.makeID();
		this.id = id;

		if (zot(globalControl)) globalControl = true;
		that.globalControl = globalControl;

		var _normalizedFrame = 0;
		var _normalizedFrames;
		this.parseFrames = function(label, startFrame, endFrame, fromDynamo) {
			var frames = [];
			var minSpeed = Number.MAX_VALUE;
			var maxSpeed = 0;
			if (zot(label)) {
				if (zot(startFrame)) startFrame = 0;
				if (zot(endFrame)) endFrame = that.totalFrames-1;
				addSequential(startFrame, endFrame);
			} else {
				if (zot(that.animations) || zot(that.animations[label])) return [];
				var a = that.animations[label];
				processAnimation(a);
			}
			function processAnimation(a) {
				if (Array.isArray(a)) {
					processArray(a);
				} else if (a.constructor == {}.constructor) {
					processObject(a);
				} else if (!isNaN(a)) {
					frames.push({f:Math.floor(a), s:1});
				}
			}
			function processArray(a) {
				addSequential(a[0], a[1], a[3]);
				if (a[2] && !zot(that.animations[a[2]])) processAnimation(that.animations[a[2]]);
			}
			function processObject(a) {
				if (zot(a.frames)) return;
				if (zot(a.speed)) a.speed = 1;
				for (var i=0; i<a.frames.length; i++) {
					if (a.speed < minSpeed) minSpeed = a.speed;
					if (a.speed > maxSpeed) maxSpeed = a.speed;
					frames.push({f:a.frames[i], s:a.speed});
				}
				if (a.next && !zot(that.animations[a.next])) processAnimation(that.animations[a.next]);
			}
			function addSequential(start, end, speed) {
				if (zot(speed)) speed = 1;
				if (end > start) {
					for (var i=start; i<=end; i++) {inner(i);}
				} else {
					for (var i=end; i<=start; i++) {inner(start-(i-end));}
				}
				function inner(i) {
					if (speed < minSpeed) minSpeed = speed;
					if (speed > maxSpeed) maxSpeed = speed;
					frames.push({f:i, s:speed});
				}
			}
			if (fromDynamo) return frames;
			// run() uses an array of frame numbers (normalized to speed) where dynamo uses the speed

			// normalize up to 1/10 - as in if put at .1 then have to multiply all others speeds by 10
			minSpeed = zim.constrain(zim.decimals(minSpeed), .1);
			maxSpeed = zim.constrain(zim.decimals(maxSpeed), .1);

			// normalize speed:
			var framesNormalized = [];
			var normalize = (minSpeed != maxSpeed);
			var fr;
			for (var i=0; i<frames.length; i++) {
				fr = frames[i];
				if (normalize) {
					// if minSpeed less than 1 then divide all others by minSpeed otherwise use speed - and need to round to a number that is at least .1
					for (var j=0; j<zim.constrain(Math.round(minSpeed<1?fr.s/minSpeed:fr.s), .1); j++) {
						framesNormalized.push(fr.f);
					}
				} else {
					framesNormalized.push(fr.f);
				}
			}
			return framesNormalized;
		}

		this.run = function(time, label, call, params, wait, waitedCall, waitedParams, loop, loopCount, loopWait, loopCall, loopParams, loopWaitCall, loopWaitParams, rewind, rewindWait, rewindCall, rewindParams, rewindWaitCall, rewindWaitParams, startFrame, endFrame, tweek, id, globalControl) {
			var sig = "time, label, call, params, wait, waitedCall, waitedParams, loop, loopCount, loopWait, loopCall, loopParams, loopWaitCall, loopWaitParams, rewind, rewindWait, rewindCall, rewindParams, rewindWaitCall, rewindWaitParams, startFrame, endFrame, tweek, id, globalControl";
			var duo; if (duo = zob(this.run, arguments, sig)) return duo;

			var obj;
			var set;
			var lookup;
			if (zot(tweek)) tweek = 1;
			if (!zot(id)) that.id = id;
			if (!zot(globalControl)) that.globalControl = globalControl;

			if (Array.isArray(label)) {
				// check labels
				var innerLabel;
				var lastLabel;
				obj = [];
				var extraTime = 0;
				var firstStartFrame;
				for (var i=0; i<label.length; i++) {
					innerLabel = label[i]; // {label:"first", time:1000, etc}

					innerLabel.lookup = that.parseFrames(innerLabel.label, innerLabel.startFrame, innerLabel.endFrame);
					if (i==0) firstStartFrame = innerLabel.lookup[0];
					delete innerLabel.startFrame;
					delete innerLabel.endFrame;

					innerLabel.obj = zim.merge(innerLabel.obj, {normalizedFrame:innerLabel.lookup.length-1});
					innerLabel.set = zim.merge(innerLabel.set, {normalizedFrames:{noPick:innerLabel.lookup}, normalizedFrame:0});

					// based on previous frames
					if (zot(innerLabel.wait)) innerLabel.wait = extraTime*tweek;

					lastLabel = innerLabel.label;
					delete innerLabel.label;

					obj.push(innerLabel);

					// will get applied next set of frames
					extraTime = 0;
					var tt = zot(innerLabel.time)?time:innerLabel.time;
					if (endFrame-startFrame > 0) extraTime = tt / (endFrame-startFrame) / 2; // slight cludge - seems to look better?

					// if (i==0) firstStartFrame = startFrame;
				}
				//startFrame = firstStartFrame;
				if (obj.length == 0) return this;
				if (obj.length == 1) { // just one label in list ;-)
					time = obj[0].time;
					label = lastLabel;
					setSingle();
				} else {
					that.gotoAndStop(firstStartFrame);
				}
			} else { // single label
				setSingle();
			}

			function setSingle() {
				_normalizedFrames = that.parseFrames(label, startFrame, endFrame);
				_normalizedFrame = 0;
				that.gotoAndStop(_normalizedFrames[_normalizedFrame]);
				startFrame = endFrame = null;
				obj = {normalizedFrame:_normalizedFrames.length-1};
			}

			if (zot(time)) time = 1000;
			// if already running the sprite then stop the last run
			if (that.running) that.stopAnimate(that.id);
			that.running = true;


			if (!Array.isArray(obj)) {
				var extraTime = 0;
				if (endFrame-startFrame > 0) extraTime = time / Math.abs(endFrame-startFrame) / 2; // slight cludge - seems to look better?
				if (_normalizedFrames && _normalizedFrames.length>0) extraTime = time / _normalizedFrames.length / 2; // slight cludge - seems to look better?
				if (zot(loopWait)) {loopWait = extraTime*tweek};
				if (zot(rewindWait)) {rewindWait = extraTime*tweek};
			}


			// locally override call to add running status after animation done
			var localCall = function() {
				if (call && typeof call == 'function') call(params||that);
				that.running = false;
				that.stop();
			}
			zim.animate({
				target:that,
				obj:obj,
				time:time,
				ease:"linear",
				call:localCall,
				params:params,
				wait:wait, waitedCall:waitedCall, waitedParams:waitedParams,
				loop:loop, loopCount:loopCount, loopWait:loopWait,
				loopCall:loopCall, loopParams:loopParams,
				loopWaitCall:loopWaitCall, loopWaitParams:loopWaitParams,
				rewind:rewind, rewindWait:rewindWait, // rewind is ignored by animation series
				rewindCall:rewindCall, rewindParams:rewindParams,
				rewindWaitCall:rewindWaitCall, rewindWaitParams:rewindWaitParams,
				override:false,
				id:that.id
			});
			that.runPaused = false;
			return that;
		}

		this.runPaused = true;
		this.pauseRun = function(paused) {
			if (zot(paused)) paused = true;
			that.runPaused = paused;
			if (that.globalControl) {
				zim.pauseAnimate(paused, that.id);
			} else {
				that.pauseAnimate(paused, that.id);
			}
			return that;
		}
		this.stopRun = function() {
			that.runPaused = true;
			that.running = false;
			if (that.globalControl) {
				zim.stopAnimate(that.id);
			} else {
				that.stopAnimate(that.id);
			}
			return that;
		}

		Object.defineProperty(this, 'frame', {
			get: function() {
				return this.currentFrame;
			},
			set: function(value) {
				value = Math.round(value);
				if (this.paused) {
					this.gotoAndStop(value);
				} else {
					this.gotoAndPlay(value);
				}
			}
		});

		Object.defineProperty(this, 'normalizedFrame', {
			get: function() {
				return _normalizedFrame;
			},
			set: function(value) {
				_normalizedFrame = Math.round(value);
				this.gotoAndStop(_normalizedFrames[_normalizedFrame]);
			}
		});

		Object.defineProperty(this, 'normalizedFrames', {
			get: function() {
				return _normalizedFrames;
			},
			set: function(value) {
				_normalizedFrames = value;
			}
		});

		Object.defineProperty(this, 'totalFrames', {
			get: function() {
				return sheet.getNumFrames();
			},
			set: function(value) {
				zog("zim.Sprite - totalFrames is read only");
			}
		});

		if (style!==false) zimStyleTransforms(this, DS); // global function - would have put on DisplayObject if had access to it
		this.clone = function() {
			return this.cloneProps(new zim.Sprite(image, cols, rows, count, offsetX, offsetY, spacingX, spacingY, width, height, animations, json, null, globalControl, spriteSheet, style, this.group, inherit));
		}
		this.localToGlobal = function(x,y) {
			return localToGlobal(x,y,this,this.cjsSprite_localToGlobal);
		}
		this.globalToLocal = function(x,y) {
			return globalToLocal(x,y,this,this.cjsSprite_globalToLocal);
		}
		this.localToLocal = function(x,y,target) {
			return localToLocal(x,y,target,this);
		}
		this.hasProp = function(prop) {
			return (!zot(this[prop]) || this.hasOwnProperty(prop))
		}
		this.dispose = function() {
			this.removeAllEventListeners();
			if (this.parent) this.parent.removeChild(this);
		}
	}
	zim.extend(zim.Sprite, createjs.Sprite, ["clone","localToLocal","localToGlobal","globalToLocal"], "cjsSprite", false);
	zimify(zim.Sprite.prototype);
	//-50.8

/*--
zim.MovieClip = function(mode, startPosition, loop, labels, style, group, inherit)

MovieClip
zim class - extends a createjs.MovieClip

DESCRIPTION
A MovieClip adds timelines to a Container.
The timelines are animate() zimTween properties.
The zimTween property returns a CreateJS Tween object.
Primarily made to support Adobe Animate MovieClip export.
*Consider this experimental for the moment...

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var movieClip = new MovieClip();
var circle = new Circle(20, blue).animate({
	props:{scale:3}, time:100, rewind:true, loop:true
});

// time is in frames NOT in ms - so 100 frames at the Ticker.framerate 60 fps by default is almost 2 seconds
// note that to change the Ticker's framerate use setFPS(mobile, desktop) method
// if you use one number then both mobile and desktop are set to that fps.
// Note that the defaults are 30 fps mobile and 60 fps desktop

movieClip.timeline.addTween(circle.zimTween);
movieClip.play();
movieClip.center();
stage.on("stagemousedown", function() {
   movieClip.paused = !movieClip.paused;
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
// from the CreateJS MovieClip docs: http://www.createjs.com/docs/easeljs/classes/MovieClip.html
mode - (default "independent") or single_frame (based on startPosition) or synched (syncs to parent)
startPosition - (default 0) the start position of the MovieClip (*could not get to work)
loop - (default true) set to false not to loop (*did not seem to loop so use loop:true in zim.animate())
labels - (default null) declare label property with position value
	eg. {explode:20} to use with gotoAndPlay("explode") rather than gotoAndPlay(20)
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for MovieClip methods, such as:
play(), gotoAndPlay(), gotoAndStop(), stop(), advance(),
on(), off(), getBounds(), setBounds(), dispatchEvent(), etc.

PROPERTIES
type - holds the class name as a String
** bounds must be set first for these to work
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for MovieClip properties, such as:
currentFrame, totalFrames, currentLabel, duration, framerate, labels, loop, mode, paused, startPosition, timeline,
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseEnabled, parent, etc.

EVENTS
See the CreateJS Easel Docs for MovieClip events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+50.9
	zim.MovieClip = function(mode, startPosition, loop, labels, style, group, inherit) {
		var sig = "mode, startPosition, loop, labels, style, group, inherit";
		var duo; if (duo = zob(zim.MovieClip, arguments, sig, this)) return duo;
		z_d("50.9");
		this.type = "MovieClip";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(mode)) mode = DS.mode!=null?DS.mode:null;
		if (zot(startPosition)) startPosition = DS.startPosition!=null?DS.startPosition:null;
		if (zot(loop)) loop = DS.loop!=null?DS.loop:null;
		if (zot(labels)) labels = DS.labels!=null?DS.labels:null;

		this.cjsMovieClip_constructor(mode, startPosition, loop, labels);

		if (style!==false) zimStyleTransforms(this, DS); // global function - would have put on DisplayObject if had access to it
		this.clone = function() {
			return this.cloneProps(new zim.MovieClip(mode, startPosition, loop, labels, style, this.group, inherit));
		}
		this.hasProp = function(prop) {
			return (!zot(this[prop]) || this.hasOwnProperty(prop))
		}
		this.localToGlobal = function(x,y) {
			return localToGlobal(x,y,this,this.cjsMovieClip_localToGlobal);
		}
		this.globalToLocal = function(x,y) {
			return globalToLocal(x,y,this,this.cjsMovieClip_globalToLocal);
		}
		this.localToLocal = function(x,y,target) {
			return localToLocal(x,y,target,this);
		}
		this.dispose = function() {
			this.removeAllEventListeners();
			if (this.parent) this.parent.removeChild(this);
		}
	}
	zim.extend(zim.MovieClip, createjs.MovieClip, ["clone","localToLocal","localToGlobal","globalToLocal"], "cjsMovieClip", false);
	zimify(zim.MovieClip.prototype);
	//-50.9

/*--
zim.SVGContainer = function(svg, splitTypes, geometric, style, group, inherit)

SVGContainer
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Parses SVG and adds items to a ZIM Container.
Items are created as ZIM Shapes: Circle, Rectangle, Blob, Squiggle.
If geometric is true then Circle and Rectangle are used otherwise Blob is used.
Items can be accessed using svgcontainer.getChildAt(0), etc.
See: https://zimjs.com/svg/
See: https://zimjs.com/explore/svgcontainer.html

An SVG path string can be passed directly to a Squiggle or Blob points parameter
and so avoiding the SVGContainer - see ZIM Squiggle and Blob

WARNING: this should be considered experimental
The idea is that paths from SVG can be made editable in ZIM
or animation, dragging, or Label along paths can be accommodated
As such, not all SVG features will work - no CSS, Text, Gradients, DropShadows, etc.
It is possible that these will be added at some point
See also the ZIM svgToBitmap() function under META to get an exact image of the SVG

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var svgContainer = new SVGContainer(frame.asset("sample.svg")).addTo();

// OR

var svg = `<svg  width="150" height="200" xmlns="h t t p ://www.w3.org/2000/svg">
    <path id="lineAB" d="M 0 0 l 150 200" stroke="red" stroke-width="3" fill="none" />
</svg>`;
var svgContainer = new SVGContainer(svg).center();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
svg - an SVG file loaded into a frame.asset() or SVG text
splitTypes - (default false) - set to true to split different types of paths into separate objects
geometric - (default true) - set to false to load Rectangle and Circle objects as Blob objects
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
processPath(path) - path is an SVG path string - returns a ZIM Blob or Squiggle points array
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
svg - a reference to the SVG text
type - holds the class name as a String
** bounds must be set first for these to work
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+50.95

	zim.SVGContainer = function(svg, splitTypes, geometric, style, group, inherit) {
		var sig = "svg, splitTypes, geometric, style, group, inherit";
		var duo; if (duo = zob(zim.SVGContainer, arguments, sig, this)) return duo;
		z_d("50.95");
		this.group = group;
		var DS = style===false?{}:zim.getStyle("SVGContainer", this.group, inherit);

		var that = this;
		var startPosition = new zim.Point(0,0); // the x,y of the last shape
		var aCommand=[];	//kv adjust logic

		if (zot(splitTypes)) splitTypes = false;
		if (zot(geometric)) geometric = true;

		if (!zot(svg)) {

			if (!zot(svg.draggable)) {
				var parser = new DOMParser();
				var svg = !svg.innerHTML?svg:parser.parseFromString(svg.innerHTML,"text/xml");
				var list = svg.getElementsByTagName("svg");
				var tag = this.svg = list?svg.getElementsByTagName("svg")[0]:null;
			} else {
				if (!svg.getAttribute) {
					var parser = new DOMParser();
					svg = parser.parseFromString(svg, "image/svg+xml").documentElement;
				}
				var tag = this.svg = svg;
			}
			var w, h;
			if (!zot(tag)){
				var w = tag.getAttribute("width");
				var h = tag.getAttribute("height");
			}
			if (w) w = Number(w.trim());
			if (h) h = Number(h.trim());
			this.zimContainer_constructor(w, h);
			this.type = "SVGContainer";

			if (zot(tag)) return;

			var defaultFill = zim.black, generalFill = zim.black;
			var defaultStroke =zim. black, generalStroke = zim.black;
			var defaultStrokeSize = 2, generalStrokeSize = 2;
			var defaultAlpha = 1, generalAlpha = 1;
			var defaultStrokeAlpha = 1, generalStrokeAlpha = 1;
			var currentTransform;

			function processTag(tag) {
				zim.loop(tag, function (t) {
					var tn = t.tagName.toLowerCase();
					if (tn == "path") processPath(t);
					if (tn == "circle") processShape("circle", t);
					if (tn == "rect") processShape("rect", t);
					if (tn == "ellipse") processShape("ellipse", t);
					if (tn == "line") processShape("line", t);
					if (tn == "polygon") processShape("polygon", t);
					if (tn == "polyline") processShape("polyline", t);
					if (tn == "g") {
						// styles can be overwritten by parameters in the general tag
						// so find styles first
						var style = t.getAttribute("style");
						var f,s,ss,a,aa;
						if (style) {
							var styles = processStyle(style);
							f = styles[0];
							s = styles[1];
							ss = styles[2];
							a = styles[3];
							aa = styles[4];
						}
						// then overwrite styles with any attribute values
						currentTransform = t.getAttribute("transform");
						generalFill = t.getAttribute("fill")?t.getAttribute("fill"):!zot(f)?f:defaultFill;
						generalStroke = t.getAttribute("stroke")?t.getAttribute("stroke"):!zot(s)?s:defaultStroke;
						generalStrokeSize = t.getAttribute("stroke-width")?t.getAttribute("stroke-width"):!zot(ss)?ss:defaultStrokeSize;
						generalAlpha = t.getAttribute("fill-opacity")?t.getAttribute("fill-opacity"):!zot(a)?a:defaultAlpha;
						generalStrokeAlpha = t.getAttribute("stroke-opacity")?t.getAttribute("stroke-opacity"):!zot(aa)?aa:defaultStrokeAlpha;
					}
					// general settings can wrap any number of tags - the tags are processed here:
					processTag(t.children);
					// after this nest of tags are processed, clear the general settings
					if (t.tagName.toLowerCase() == "g") {
						generalFill = defaultFill;
						generalStroke = defaultStroke;
						generalStrokeSize = defaultStrokeSize;
						generalAlpha = defaultAlpha;
						generalStrokeAlpha = defaultStrokeAlpha;
						currentTransform = null;
					}
				})
			}
			var process = svg.getElementsByTagName("svg");
			if (process.length == 0) process = [svg];

			function processStyle(style) {
				var st = style.split(";");											//kv note: there si bug when style contains ; at the end of the string
				var f,s,ss,a,aa;
				loop(st, function (sty) {
					var sty = sty.replace(/,/g,"");
					var styl = sty.split(":");
					var prop = styl[0].trim().toLowerCase();
					var val = styl[1].trim().toLowerCase().replace("px", "");
					if (prop=="fill") f = val;
					if (prop=="stroke") s = val;
					if (prop=="stroke-width") ss = val;
					if (prop=="opacity") a = val, aa = val;
					if (prop=="fill-opacity") a = val;
					if (prop=="stroke-opacity") aa = val;
				});
				return [f,s,ss,a,aa];
			}

			function processShape(type, tag) {
				var shape;
				var g = processGeneral(tag); // want ES6
				var f = g[0], s = g[1], ss = g[2], a = g[3], aa = g[4], x = g[5], y = g[6];
				if (type == "circle") {
					var r = Number(tag.getAttribute("r").trim());
					var d = r*.5523;
					if (geometric) shape = new zim.Circle(Number(tag.getAttribute("r")), f, s, ss);
					else shape = new zim.Blob(f, s, ss, 4, r, d, "mirror");
				} else if (type == "rect") {
					if (geometric) shape = new zim.Rectangle(Number(tag.getAttribute("width")), Number(tag.getAttribute("height")), f, s, ss, Number(tag.getAttribute("rx")));
					else {
						var w = Number(tag.getAttribute("width"));
						var h = Number(tag.getAttribute("height"));
						var rx = Number(tag.getAttribute("rx"));
						var ry = Number(tag.getAttribute("ry"));
						if (rx && ry) {
							var dx = rx*.5523;
							var dy = ry*.5523;
							shape = new zim.Blob(f, s, ss, [
								[rx,0,0,0,-dx,0,0,0,"free"],[w-rx,0,0,0,0,0,dx,0,"free"],
								[w,ry,0,0,0,-dy,0,0,"free"],[w,h-ry,0,0,0,0,0,dy,"free"],
								[w-rx,h,0,0,dx,0,0,0,"free"],[rx,h,0,0,0,0,-dx,0,"free"],
								[0,h-ry,0,0,0,dy,0,0,"free"],[0,ry,0,0,0,0,0,-dy,"free"]]);
						} else {
							shape = new zim.Blob(f, s, ss, [[0,0],[w,0],[w,h],[0,h]]);
						}
					}
				} else if (type == "line") {
					shape = new zim.Squiggle(s, ss, [[Number(tag.getAttribute("x1")), Number(tag.getAttribute("y1"))],[Number(tag.getAttribute("x2")), Number(tag.getAttribute("y2"))]]);
				} else if (type == "polygon" || type == "polyline") {
					var p = tag.getAttribute("points");
					p = p.replace(/-/g, " -");
					p = p.replace(/\s+/g, " ");
					var points = [];
					loop(p.split(" "), function (point) {
						var pp = point.split(",");
						points.push([Number(pp[0].trim()), Number(pp[1].trim())]);
					})
					if (type=="polygon") shape = new Blob(f, s, ss, points);
					else shape = new zim.Squiggle(s, ss, points);
				} else if (type == "ellipse") {
					shape = new zim.Blob(f, s, ss, ellipse(0, 0, Number(tag.getAttribute("rx")), Number(tag.getAttribute("ry"))));
				}
				shape.loc(x,y,that);
				var transform = tag.getAttribute("transform");
				if (transform || currentTransform) processTransform(shape, transform || currentTransform);
				if (shape.type == "Rectangle" || shape.type=="Circle") shape.transform({showReg:false})
			}

			function processTransform(shape, transform) {
				var tr = transform.split(")");
				// apply all transforms in the order given
				loop(tr, function (tra) {
					if (tra=="") return;
					var tran = tra.trim().split("(");
					var prop = tran[0].trim().toLowerCase();
					var val = tran[1].trim().toLowerCase().replace("px", "").replace("deg", "");
					if (prop=="translate") {
						var m = val.split(",");
						shape.mov(Number(m[0].trim()), m[1]?Number(m[1].trim()):0);
					}
					if (prop=="scale"){
						var s = val.split(",");
						if (shape.type=="Blob" || shape.type=="Squiggle") {
							if (s.length == 1) shape.transformPoints("scale", Number(s[0].trim()))
							else if (s.length == 2) {
								shape.transformPoints("scaleX", Number(s[0].trim()));
								shape.transformPoints("scaleY", Number(s[1].trim()));
							}
						} else {
							if (s.length == 1) shape.sca(Number(s[0].trim()))
							else if (s.length == 2) {
								shape.sca(Number(s[0].trim()), Number(s[1].trim()));
							}
						}
					}
					if (prop=="rotate") {
						var r = val.split(",");
						// if (shape.type=="Blob" || shape.type=="Squiggle") {
							// rotation is a different way for SVG and transform() - too bad
							// it rotates around 0,0 unless a different point is chosen
							// so shape.transformPoints which is a registration point system
							// is unlikely to work - and too complex to add rotate around a given point
							// so we will use zim rot() to which we have added rotating around a different point
							// but this will rotate the little box handles
							// maybe look into keeping those parallel in the blob and squiggle - no matter what the rotation
							// if (r.length == 1) shape.transformPoints("rotation", Number(r[0].trim()))
						if (r.length == 1) r.push(0,0);
						else if (r.length == 2) r.push(0);
						shape.rot(Number(r[0].trim()), Number(r[1].trim()), Number(r[2].trim()));
					}

					if (prop=="skewX") shape.skewX = val;
					if (prop=="skewY") shape.skewY = val;
				});
			}

			// beatgammit on StackOverflow
			function ellipse(x, y, xDis, yDis) {
				var kappa = 0.5522848, // 4 * ((âˆš(2) - 1) / 3)
					ox = xDis * kappa,  // control point offset horizontal
					oy = yDis * kappa,  // control point offset vertical
					xe = x + xDis,      // x-end
					ye = y + yDis;      // y-end
				var points = [ // modified by Dan Zen to relative
					[x - xDis, y, 0, 0, x, ye+oy-yDis, x, y-oy, "mirror"],
					[x, y - yDis, 0, 0, x-ox, y, xe+ox-xDis, y, "mirror"],
					[xe, y, 0, 0, x, y-oy, x + ox-ox, ye+oy-yDis, "mirror"],
					[x, ye, 0, 0, xe-xDis+ox, y, x-ox, y, "mirror"]
				];
				return points;
			}

			processTag(process);

		}

		function processGeneral(tag) {
			// any styles on the tag overwrites general styles or attributes
			var f,s,ss,a,aa;
			var style = tag.getAttribute("style");
			if (style) {
				var styles = processStyle(style);
				f = styles[0];
				s = styles[1];
				ss = styles[2];
				a = styles[3];
				aa = styles[4];
			}
			// kv comments: need to apply string cleansing, application bugs when there is a semi colon character used in tags.
			// get rid of semi colon,
			// any attributes on the tag overwrites styles or general
			f = tag.getAttribute("fill")?tag.getAttribute("fill"):!zot(f)?f:generalFill;
			s = tag.getAttribute("stroke")?tag.getAttribute("stroke"):!zot(s)?s:generalStroke;
			ss = tag.getAttribute("stroke-width")?tag.getAttribute("stroke-width"):!zot(ss)?ss:generalStrokeSize;
			a = tag.getAttribute("fill-opacity")?tag.getAttribute("fill-opacity"):!zot(a)?a:generalAlpha;
			aa = tag.getAttribute("stroke-opacity")?tag.getAttribute("stroke-opacity"):!zot(aa)?aa:generalStrokeAlpha;

			var x = tag.getAttribute("x")?tag.getAttribute("x"):0;
			x = tag.getAttribute("cx")?tag.getAttribute("cx"):x;
			var y = tag.getAttribute("y")?tag.getAttribute("y"):0;
			y = tag.getAttribute("cy")?tag.getAttribute("cy"):y;
			if (!zot(a) && !zot(f)) f = zim.convertColor(f, "rgba", Number(a));
			if (!zot(aa) && !zot(s)) s = zim.convertColor(s, "rgba", Number(aa));
			return [f,s,Number(ss),Number(a),Number(aa),Number(x),Number(y)];
		}

		function processPath (path, make) {
			if (zot(make)) make = true;
			var commands = ["M","m","L","l","H","h","V","v","C","c","S","s","Q","q","T","t","A","a","z","Z"];
			var commandsRelative = ["m","l","h","v","c","s","q","t","a","z"];

			var position = new zim.Point(0,0); // the current position - relative places based on this

			if (zot(path.getAttribute)) {
				var d = path;
			} else {
				var id = path.getAttribute("id");
				var d = path.getAttribute("d");
			}

			// m251.85 119.04c7.85 10.45-9.81
			d = d.replace(/,/g ," ");
			d = d.replace(/([a-zA-Z])/g, " $1 ");
			d = d.replace(/-/g, " -");
			d = d.replace(/\s+/g, " ");


			if (make) {
				var g = processGeneral(path); // want ES6
				var f = g[0]; var s = g[1]; var ss = g[2]; var a = g[3]; var aa = g[4];
			}

			var shape;																									//kv adjust logic
			var aNumber;																								//kv adjust logic
			//var points = [[0,0]];																						//kv adjust logic
			var points = [];																							//kv adjust logic
			var lastTempPoint = [0,0];																					//kv adjust logic
			var line = new zim.Point(0,0);
			var quad = new zim.Point(0,0,0,0);
			var cube = new zim.Point(0,0,0,0,0,0);
			var arc = new zim.Point(0,0,0,0,0,0,0);
			//kv var data = d.split(" ");																				//kv adjust logic
			var dataOrigin = d.split(" ");																				//kv adjust logic
			var data = dataOrigin.slice(1,dataOrigin.length);															//kv adjust logic
			var aCommand = [];																							//kv adjust logic
			var missingCommand = false;													//not used yet					//kv adjust logic
			var lastCommand = "";																						//kv adjust label
			var previousCommand = "";													//not used yet					//kv adjust logic
			var adding = false;
			var what;
			var type = "squiggle";
			var dataType = null;
			//kv loop(data, function (command) {					//kv adjust logic
			zim.loop(data, function (command,i) {
																					//kv adjust logic
				if (i==0) {startPosition.x = 0; startPosition.y=0;lastTempPoint = [0,0];lastCommand = "";previousCommand = ""};
				if (commands.indexOf(command) == -1) {
					if (what == "lxo") {what="lx"; aCommand.push("l");missingCommand=true};								//kv adjust logic
					if (what == "lyo") {what="ly"; missingCommand=true};												//kv adjust logic
					if (what == "Lxo") {what="Lx"; aCommand.push("L");missingCommand=true};								//kv adjust logic
					if (what == "Lyo") {what="Ly"; missingCommand=true};												//kv adjust logic
					aNumber = Number(command);																			//kv adjust logic
					aNumber = Math.round(aNumber * 100) / 100;															//kv adjust logic
					// position
					if (what == "X") {
						startPosition.x = aNumber;
						what = "Y";
					} else if (what == "Y") {
						startPosition.y = aNumber;
						//kv what = "Lx"; // in case no letters come next												//kv adjust logic
						what = "Lxo"; // in case no letters come next													//kv adjust logic
						points.push([startPosition.x, startPosition.y]);												//kv adjust logic
						// what = null;
					} else if (what == "x") {
						//if (points.length > 1) {
						//	startPosition.x = points[points.length-1][0]; 												//kv adjust logic
						//	startPosition.y = points[points.length-1][1]												//kv adjust logic
						//} else {
						//	startPosition.x = lastTempPoint[0];															//kv adjust logic
						//	startPosition.y = lastTempPoint[1]															//kv adjust logic
						//};
						startPosition.x = startPosition.x+aNumber;
						what = "y";
					} else if (what == "y") {
						startPosition.y = startPosition.y+aNumber;
						what = "lxo";																					//kv adjust logic
						//kv what = "lx"; // in case no letters come next												//kv adjust logic
						points.push([startPosition.x, startPosition.y]);												//kv adjust logic
						// what = null;
					}

					// left, right, top or bottom
					if (what == "H" || what == "h") {
						position.x = points[points.length-1][0];														//kv adjust logic
						position.y = points[points.length-1][1];														//kv adjust logic
						position.x = position.x + (what=="h"?aNumber:aNumber-startPosition.x);
						points.push([position.x, position.y]);
						what = "Lx"
					} else if (what == "V" || what == "v") {
						position.x = points[points.length-1][0];														//kv adjust logic
						position.y = points[points.length-1][1];														//kv adjust logic
						position.y = position.y + (what=="v"?aNumber:aNumber-startPosition.y);
						points.push([position.x, position.y]);
						what = "lx"
					}

					// line
					if (what == "Lx") {
						//kv line.x = aNumber-startPosition.x;							//kv adjust logic
						line.x = aNumber;												//kv adjust logic
						what = "Ly";
					} else if (what == "Ly") {
						//kv line.y = aNumber-startPosition.y;							//kv adjust logic
						line.y = aNumber;												//kv adjust logic
						position.x = line.x;
						position.y = line.y;
						points.push([position.x, position.y]);
						what = "Lx"
					} else if (what == "lx") {
						if (aCommand.length > 0) {										//kv adjust logic
							if (position.x != 0 && position.y != 0 ) {
								startPosition.x = position.x;
								startPosition.y = position.y
							}
							else {
							startPosition.x = position.x + startPosition.x;				//kv adjust logic
							startPosition.y = position.y + startPosition.y;				//kv adjust logic
							//startPosition.x = position.x + lastTempPoint[0];			//kv adjust logic
							//startPosition.y = position.y + lastTempPoint[1];			//kv adjust logic
							}
						};																//kv adjust logic
						line.x = startPosition.x + aNumber;								//kv adjust logic
						//kv line.x = line.x + aNumber;									//kv adjust logic
						if (missingCommand) {what="lyo"} else							//kv adjust logic
						what = "ly";
					} else if (what == "ly") {
						line.y = startPosition.y + aNumber;								//kv adjust logic
						//line.y = line.y + aNumber;									//kv adjust logic
						position.x = line.x;
						position.y = line.y;
						points.push([position.x, position.y]);
						what = "lx";
					}


					// Quadratic
					if (what == "qx" || what == "Qx") {
						if (points.length > 0) {								//kv adjust logic
							startPosition.x = points[points.length-1][0]; 		//kv adjust logic
							startPosition.y = points[points.length-1][1]		//kv adjust logic
						} else {												//kv adjust logic
							startPosition.x = 0;						 		//kv adjust logic
							startPosition.y = 0									//kv adjust logic
						};														//kv adjust logic

						//kv quad.x = what=="qx"?position.x+aNumber:aNumber-startPosition.x;
						quad.x = what=="qx"?startPosition.x+aNumber:aNumber;	//kv adjust logic
						what = what=="qx"?"qy":"Qy";
					} else if (what == "qy" || what == "Qy") {
						//kv quad.y = what=="qy"?position.y+aNumber:aNumber-startPosition.y;
						quad.y = what=="qy"?startPosition.y+aNumber:aNumber;	//kv adjust logic
						what = what=="qy"?"qz":"Qz";
						if (adding) {
							adding = false;
							var lastPoint = points[points.length-1];
							lastPoint[6] = -lastPoint[4];
							lastPoint[7] = -lastPoint[5];
							lastPoint[8] = "mirror";
							position.x = quad.x;
							position.y = quad.y;
							points[points.length] = [
								position.x, position.y,
								0, 0,
								-lastPoint[6], lastPoint[7],
								0,0,
								"free"
							];
							what = what=="qy"?"qx":"Qx";
						}
					} else if (what == "qz" || what == "Qz") {
						//kv quad.z = what=="qz"?position.x+aNumber:aNumber-startPosition.x;
						quad.z = what=="qz"?startPosition.x+aNumber:aNumber;							//kv adjust logic
						what = what=="qz"?"qq":"Qq";
					} else if (what == "qq" || what == "Qq") {
						//kv quad.w = what=="qq"?position.y+aNumber:aNumber-startPosition.y;
						quad.w = what=="qq"?startPosition.y+aNumber:aNumber;							//kv adjust logic
                        var lastPoint = points[points.length-1];
                        position.x = lastPoint[0]; 													    //kv adjust logic
                        position.y = lastPoint[1];													    //kv adjust logic
						if (points.length == 1) {														//kv adust logic
							lastPoint[2] = 0; lastPoint[3] = 0; lastPoint[4] = 0; lastPoint[5] = 0;		//kv adjust logic    																	//kv debug
							lastPoint[6] = 2/3 *(quad.x - position.x); // relative needs position and absolute does not it
							lastPoint[7] = 2/3 *(quad.y - position.y);
							lastPoint[8] = "free"
						}																				//kv adjust logic
						else {																			//kv adjust logic
							points[points.length] = [													//kv adjust logic
								position.x, position.y,													//kv adjsut logic
								0, 0, 0, 0,																//kv adjust logic
								2/3*(quad.x-position.x), 												//kv adjust logic
								2/3*(quad.y-position.y),												//kv adjust logic
								"free"																	//kv adjust logic
							]																			//kv adjust logic
						};																				//kv adjust logic
						position.x = quad.z; // assign this point's position - fix this and apply throughout...
						position.y = quad.w;
						points[points.length] = [
							position.x, position.y,
							0, 0,
							// relative needs - startPosition and absolute does not
							2/3*(quad.x-position.x), 2/3*(quad.y-position.y),
							0,0,
							"free"
						];
						what = what=="qq"?"qx":"Qx"
					};
					// Cubic
					if (what == "cx" || what == "Cx") {
						if (points.length > 0) {														//kv adjust logic
							startPosition.x = points[points.length-1][0]; 								//kv adjust logic
							startPosition.y = points[points.length-1][1]								//kv adjust logic
						} else {																		//kv adjust logic
							startPosition.x = 0;						 								//kv adjust logic
							startPosition.y = 0															//kv adjust logic
						};																				//kv adjust logic

						//kv cube.x = what=="cx"?position.x+aNumber:aNumber-startPosition.x;
						cube.x = what=="cx"?startPosition.x+aNumber:aNumber;							//kv adjust logic
						what = what=="cx"?"cy":"Cy";
					} else if (what == "cy" || what == "Cy") {		// y Control Point 1
						//kv cube.y = what=="cy"?position.y+aNumber:aNumber-startPosition.y;
						cube.y = what=="cy"?startPosition.y+aNumber:aNumber;							//kv adjust logic
						what = what=="cy"?"cz":"Cz";
					} else if (what == "cz" || what == "Cz") {		// x Control Point 2
						//kv cube.z = what=="cz"?position.x+aNumber:aNumber-startPosition.x;
						cube.z = what=="cz"?startPosition.x+aNumber:aNumber;							//kv adjust logic
						what = what=="cz"?"cq":"Cq";
					} else if (what == "cq" || what == "Cq") {		// y Control Point 2
						//kv cube.q = what=="cq"?position.y+aNumber:aNumber-startPosition.y;
						cube.q = what=="cq"?startPosition.y+aNumber:aNumber;							//kv adjust logic
						what = what=="cq"?"cr":"Cr";
						if (adding) {
							if (lastCommand=="s") {previousCommand = "s"} else {previousCommand = "S"};
							//kv adding = false;
							//kv update previous point
							var lastPoint = points[points.length-1];
								lastPoint[2] = 0;							//kv adjust logic
								lastPoint[3] = 0;							//kv adjust logic
								if(zot(lastPoint[4])) {lastPoint[4] =0};	//kv adjust logic
								if(zot(lastPoint[5])) {lastPoint[5] =0};	//kv adjust logic
								lastPoint[6] = -lastPoint[4];
								lastPoint[7] = -lastPoint[5];
								// lastPoint[6] = - (cube.x-cube.z);			//kv adjust logic
								// lastPoint[7] = - (cube.y-cube.q);			//kv adjust logic
								lastPoint[8] = "mirror";
								//kv create very lastPoint
								position.x = cube.z;
								position.y = cube.q;
								points[points.length] = [
									position.x, position.y,
									0, 0,
									cube.x-position.x, cube.y-position.y,
									0,0,
									"free"
								];
							//kv what = what=="cq"?"cx":"Cx";
							what = what=="cr"?"cx":"Cx"																			//kv adjust logic
						}
					} else if (what == "cr" || what == "Cr") {
						//kv cube.r = what=="cr"?position.x+aNumber:aNumber-startPosition.x;
						cube.r = what=="cr"?startPosition.x+aNumber:aNumber;													//kv adjust logic
						what = what=="cr"?"cs":"Cs";
					} else if (what == "cs" || what == "Cs") {																	// y 2nd Point
						//kv cube.s = what=="cs"?position.y+aNumber:aNumber-startPosition.y;
						cube.s = what=="cs"?startPosition.y+aNumber:aNumber;													//kv adjust logic
						//points.push([cube.r, cube.s]);																		//kv adjust logic
						//[controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType],					//kv debug
						var lastPoint = points[points.length-1];
						if (points.length == 1) {lastPoint[2] = 0; lastPoint[3] = 0; lastPoint[4] = 0; lastPoint[5] = 0};		//kv adjust logic
						lastPoint[6] = cube.x-lastPoint[0];
						lastPoint[7] = cube.y-lastPoint[1];
						lastPoint[8] = "free";
						position.x = cube.r;
						position.y = cube.s;
						points[points.length] = [
							position.x, position.y,
							0, 0,
							cube.z-position.x, cube.q-position.y,
							0,0,
							"free"
						];
						what = what=="cs"?"cx":"Cx";
					}

					// // Arc - TODO - equation does not seem to work
					// // https://github.com/colinmeinke/svg-arc-to-cubic-bezier/issues/7
					// // rx ry x-axis-rotation large-arc-flag sweep-flag x y
					// if (what == "ax" || what == "Ax") {
					// 	arc.x = aNumber; // radius x
					// 	what = what=="ax"?"ay":"Ay";
					// } else if (what == "ay" || what == "Ay") {
					// 	arc.y = aNumber; // radius y
					// 	what = what=="ay"?"az":"Az";
					// } else if (what == "az" || what == "Az") {
					// 	arc.z = aNumber; // x-axis-rotation
					// 	what = what=="az"?"aq":"Aq";
					// } else if (what == "aq" || what == "Aq") {
					// 	arc.q = aNumber; // large-arc-flag
					// 	what = what=="aq"?"ar":"Ar";
					// } else if (what == "ar" || what == "Ar") {
					// 	arc.r = aNumber; // sweep-flag
					// 	what = what=="ar"?"as":"As";
					// } else if (what == "as" || what == "As") {
					// 	arc.s = what=="as"?position.x+aNumber:aNumber-startPosition.x;
					// 	//zog(arc.s)
					// 	what = what=="as"?"at":"At";
					// } else if (what == "at" || what == "At") {
					// 	arc.t = what=="at"?position.y+aNumber:aNumber-startPosition.y;
					// 	//zog(arc.t)
					// 	// rx ry x-axis-rotation large-arc-flag sweep-flag x y
					//
					// 	zta(arc)
					// 	var curves = arcToBezier({
					// 	  px: position.x,
					// 	  py: position.y,
					// 	  cx: arc.s,
					// 	  cy: arc.t,
					// 	  rx: arc.x,
					// 	  ry: arc.y,
					// 	  xAxisRotation: arc.z,
					// 	  largeArcFlag: arc.q,
					// 	  sweepFlag: arc.r,
					// 	});
					//
					// 	//zta(curves)
					//
					// 	loop(curves, function (curve) {
					// 		lastPoint = points[points.length-1];
					// 		lastPoint[6] = curve.x1-position.x;
					// 		lastPoint[7] = curve.y1-position.y;
					// 		lastPoint[8] = "free";
					// 		//zog(curve.x, curve.y)
					// 		position.x = curve.x;
					// 		position.y = curve.y;
					// 		points[points.length] = [
					// 			position.x, position.y,
					// 			0, 0,
					// 			curve.x2-position.x, curve.y2-position.y,
					// 			0,0,
					// 			"free"
					// 		];
					// 	});
					// 	what = what=="at"?"ax":"Ax";
					// }
				}
				else {
					aCommand.push(command);													// kv adjust logic

					// Commands
					if (command != "s") {
						adding = false;
					} 								// kv adjust logic
					if (aCommand.length > 1) {												// kv adjust logic
						// if (aCommand[aCommand.length-2] != command) {
						if (command=="M" || command=="m") {
							makeShape(aCommand);
							if (command=="M") {startPosition.x = 0; startPosition.y=0}
							else {
								startPosition.x = points[points.length-1][0]; startPosition.y=points[points.length-1][1];
							};
							points = [];
							aCommand = [];
							aCommand.push(command)
						}
					};																		// kv adjust logic

					if (command=="M") {
						what = "X";
					} else if (command=="m") {
						what = "x";
					} else if (command=="L") {
						what = "Lx";
						if (splitTypes && dataType && (dataType != "l")) makeShape(aCommand);
						dataType = "l";
					} else if (command=="l") {
						what = "lx";
						if (splitTypes && dataType && (dataType != "l")) makeShape(aCommand);
						dataType = "l";
					} else if (command=="H") {
						what = "H";
						if (splitTypes && dataType && (dataType != "l")) makeShape(aCommand);
						dataType = "l";
					} else if (command=="h") {
						what = "h";
						if (splitTypes && dataType && (dataType != "l")) makeShape(aCommand);
						dataType = "l";
					} else if (command=="V") {
						what = "V";
						if (splitTypes && dataType && (dataType != "l")) makeShape(aCommand);
						dataType = "l";
					} else if (command=="v") {
						what = "v";
						if (splitTypes && dataType && (dataType != "l")) makeShape(aCommand);
						dataType = "l";
					} else if (command=="C") {
						what = "Cx";
						if (splitTypes && dataType && (dataType != "c")) makeShape(aCommand);
						dataType = "c";
					} else if (command=="c") {
						what = "cx";
						if (splitTypes && dataType && (dataType != "c")) makeShape(aCommand);
						dataType = "c";
					} else if (command=="S") {
						adding = true;
						what = "Cx";
						if (splitTypes && dataType && (dataType != "c")) makeShape(aCommand);
						dataType = "c";
					} else if (command=="s") {
						adding = true;
						what = "cx";
						if (splitTypes && dataType && (dataType != "c")) makeShape(aCommand);
						dataType = "c";
					} else if (command=="Q") {
						what = "Qx";
						if (splitTypes && dataType && (dataType != "q")) makeShape(aCommand);
						dataType = "q";
					} else if (command=="q") {
						what = "qx";
						if (splitTypes && dataType && (dataType != "q")) makeShape(aCommand);
						dataType = "q";
					} else if (command=="T") {
						adding = true;
						what = "Qx";
						if (splitTypes && dataType && (dataType != "q")) makeShape(aCommand);
						dataType = "q";
					} else if (command=="t") {
						adding = true;
						what = "qx";
						if (splitTypes && dataType && (dataType != "q")) makeShape(aCommand);
						dataType = "q";
					} else if (command=="A") {
						type = null;
					// 	what = "Ax";
					// 	if (splitTypes && dataType && (dataType != "a")) makeShape();
					// 	dataType = "a";
					// } else if (command=="a") {
					// 	what = "ax";
					// 	if (splitTypes && dataType && (dataType != "a")) makeShape();
					// 	dataType = "a";
					} else if (command=="z" || command=="Z") {
						type = "blob";
					}

				}	// end of Command process

			}); // end of data loop

			function makeShape(myCommand, interest) {
				var myCommand;
				//var shape;																		//kv adjust logic
				lastCommand = aCommand[aCommand.length-1];																//kv adjust logic
				if (points.length >= 2)
					// M 100 350 l 150 -300
					if (lastCommand == "z" || lastCommand == "Z") {type = "blob"};										//kv adjust logic
					//if (zot(shape)) {																					//kv adjust logic
						if (type == "squiggle") shape = new zim.Squiggle(s, ss, points);
						else shape = new zim.Blob(f, s, ss, points);
						shape.loc(0,0,that);																					//kv adjust logic
					//} else {																							//kv adjust logic
					//	var dataPointsArray = [];																		//kv adjust logic
					//	dataPointsArray = shape.recordPoints().concat(points);											//kv adjust logic
					//	//shape.removeFrom();																			//kv adjust logic
					//	if (type == "squiggle") shape = new Squiggle(s, ss, dataPointsArray)							//kv adjust logic
					//	else shape = new Blob(f, s, ss, dataPointsArray)												//kv adjust logic
					//};
					//kv shape.loc(startPosition.x, startPosition.y, that);												//kv adjust logic
					//kv startPosition.x = startPosition.x + position.x;												//kv adjust logic
					//kv startPosition.y = startPosition.y + position.y;												//kv adjust logic
					lastCommand = aCommand[aCommand.length-1];															//kv adjust logic
					previousCommand = aCommand[aCommand.length-2];														//kv adjust logic
					if (commandsRelative.indexOf(lastCommand) >= 0) {
						//if (aCommand[aCommand.length-1] == "m") {														//kv adjust logic
						lastTempPoint[0] = points[points.length-1][0]; lastTempPoint[1] = points[points.length-1][1];	//kv adjust logic
						startPosition.x = points[points.length-1][0]; startPosition.y = points[points.length-1][1];		//kv adjust logic
						points = [];																					// LATEST CHANGES
						points.push([lastTempPoint[0], lastTempPoint[1]]);												//kv adjust logic
					} else {lastTempPoint = [0,0]; points = [[0,0]]};

					aCommand = [];																						//kv adjust logic
					if (lastCommand != "z" && lastCommand != "Z") aCommand.push(lastCommand);							//kv adjust logic

					position.x = 0;
					position.y = 0;
					//kv points = [[0,0]];

				var transform = path.getAttribute("transform");
				if (transform || currentTransform) processTransform(shape, transform || currentTransform);
			};

			if (make) makeShape();
			return points;

		} // end process path

		that.processPath = function(path) {
			return processPath(path,false);
		}

		if (style!==false) zimStyleTransforms(this, DS); // global function - would have put on DisplayObject if had access to it
		this.clone = function() {
			return that.cloneProps(new zim.SVGContainer(svg, splitTypes, geometric, style, this.group, inherit));
		}
	}
	zim.extend (zim.SVGContainer, zim.Container, "clone", "zimContainer", false);
	//-50.95


// SUBSECTION ZIM SHAPES

/*--
zim.Circle = function(radius, color, borderColor, borderWidth, dashed, percent, strokeObj, style, group, inherit)

Circle
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Makes a circle shape inside a container.
The registration and origin will be the center.
NOTE: mouseChildren is turned to false for all zim Shape containers.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage);

// or with 10 pixel grey stroke
var circle = new Circle(50, "red", "#666", 10);

// fill the circle with a radial gradient fill
circle.colorCommand.radialGradient([yellow,green], [0, .7], 0, 0, 20, 0, 0, 50);

// make a half circle - or any percent of a circle
var semi = new Circle({radius:200, color:pink, percent:50}).center();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
radius - |ZIM VEE| (default 50) the radius (from the center to the edge or half the diameter) ;-)
color - |ZIM VEE| (default "black") the fill color as any CSS color including "rgba()" for alpha fill (set a to 0 for tranparent fill)
borderColor - |ZIM VEE| (default null) the stroke color
borderWidth - |ZIM VEE| (default 1 if stroke is set) the size of the stroke in pixels
dashed - (default false) set to true for dashed border (if borderWidth or borderColor set)
percent - (default 100) set to a percentage of a circle (arc) - registration stays at radius center, bounds shrink to arc
strokeObj - (default {caps:"butt", joints:"miter", miterLimit:10, ignoreScale:false}) set to adjust stroke properties
	// note, not all applicable to a Circle - perhaps just ignoreScale...
	caps options: "butt", "round", "square" or 0,1,2
	joints options: "miter", "round", "bevel" or 0,1,2
	miterLimit is the ration at which the mitered joint will be clipped
	ignoreScale set to true will draw the specified line thickness regardless of object scale
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
linearGradient([colors],[ratios], x0,y0, x1,y1) - shortcut to colorCommand linearGradient method (see properties below)
radialGradient([colors],[ratios], x0,y0,radius0, x1,y1,radius1) - shortcut to colorCommand radialGradient method (see properties below)
setColorRange(color1, color2) - set a color range for shape - used by colorRange property - returns obj for chaining
	if one color is used, the current color is used and color1 is the second color in the range
cache(see Container docs for parameter description) - overrides CreateJS cache() and returns object for chaining
	Leave parameters blank to cache bounds of shape (plus outer edge of border if borderWidth > 0)
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy of the shape
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
shape - gives access to the circle shape
color - get and set the fill color
colorRange - if setColorRange() is used, the colorRange is a ratio (0-1) between the colors
	setting the colorRange will change the color property of the shape
	for instance, shape.setColorRange(blue, pink) then shape.colorRange = .5
	will set the color of the shape to half way between blue and pink
	shape.animate({color:red}, 1000); is a shortcut to animate the colorRange
	shape.wiggle("colorRange", .5, .2, .5, 1000, 5000) will wiggle the colorRange
colorCommand - access to the CreateJS fill command for bitmap, linearGradient and radialGradient fills
	eg. var shape = new Circle(20);
	// colors,ratios, x0,y0,radius0, x1,y1,radius1
	// purple on outside with pink highlight right top corner
	shape.colorCommand.radialGradient([purple,pink],[0,.8], 0,0,20, 5,-8,0);
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Fill.html
borderColor - get and set the stroke color
borderColorCommand - access to the CreateJS stroke command for bitmap, linearGradient and radialGradient strokes
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Stroke.html
borderWidth - get and set the stroke size in pixels
borderWidthCommand - access to the CreateJS stroke style command (width, caps, joints, miter, ignoreScale)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeStyle.html
borderDashedCommand - access to the CreateJS stroke dashed command (segments, offset)
	see https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeDash.html
radius - gets or sets the radius.
	The radius is independent of scaling and can be different than the width/2
	Setting the radius redraws the circle but any current scaling is kept
** setting widths, heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
mouseChildren - set to false so you do not drag the shape inside the circle
	if you nest things inside and want to drag them, will want to set to true
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+51
	zim.Circle = function(radius, color, borderColor, borderWidth, dashed, percent, strokeObj, style, group, inherit) {
		var sig = "radius, color, borderColor, borderWidth, dashed, percent, strokeObj, style, group, inherit";
		var duo; if (duo = zob(zim.Circle, arguments, sig, this)) return duo;
		z_d("51");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Circle";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);
		if (zot(radius)) radius = DS.radius!=null?DS.radius:50;
		if (zot(dashed)) dashed = DS.dashed!=null?DS.dashed:false;
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:null;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(color)) color = DS.color!=null?DS.color:(borderWidth>0?"rgba(0,0,0,0)":"black");
		if (zot(percent)) percent = DS.percent!=null?DS.percent:100;
		if (zot(strokeObj)) strokeObj = DS.strokeObj!=null?DS.strokeObj:{};

		// PICK
		var oa = remember(radius, color, borderColor, borderWidth, percent);
		function remember() {return arguments;} // for cloning PICK
		radius = zim.Pick.choose(radius);
		color = zim.Pick.choose(color);
		borderColor = zim.Pick.choose(borderColor);
		borderWidth = zim.Pick.choose(borderWidth);
		percent = zim.Pick.choose(percent);

		var that = this;
		var _radius = radius;
		var _color = color;
		var _borderColor = borderColor;
		var _borderWidth = borderWidth;
		this.mouseChildren = false;

		var circle = this.shape = new createjs.Shape();
		this.addChild(circle);

		var g = circle.graphics;
		var colorObj;
		var borderColorObj;
		var borderWidthObj;
		var borderDashedObj;
		drawShape();
		function drawShape() {
			g.c();
			that.colorCommand = colorObj = g.f(_color).command;
			// border of 0 or a string value still draws a border in CreateJS
			if (zot(_borderWidth) || _borderWidth > 0) { // no border specified or a border > 0
				if (!zot(_borderColor) || !zot(_borderWidth)) { // either a border color or thickness
					if (zot(_borderColor)) _borderColor = "black";
					that.borderColorCommand = borderColorObj = g.s(_borderColor).command;
					that.borderWidthCommand = borderWidthObj = g.ss(_borderWidth, strokeObj.caps, strokeObj.joints, strokeObj.miterLimit, strokeObj.ignoreScale).command;
					if (dashed) that.borderDashedCommand = borderDashedObj = g.sd([10, 10], 5).command;
				}
			}
			var h = _radius*2;
			if (typeof percent == "number" && percent >= 0 && percent < 100) {
				var p = 360*percent/100/2
				g.arc(0, 0, _radius, (-p-90)*Math.PI/180, (p-90)*Math.PI/180, false).cp();
				h = _radius-Math.cos(p*Math.PI/180)*_radius;
			} else {
				g.dc(0,0,_radius);
			}
			that.setBounds(-_radius,-_radius,_radius*2,h);
		}

		Object.defineProperty(that, 'color', {
			get: function() {
				return _color;
			},
			set: function(value) {
				if (zot(value)) value = "black";
				_color = value;
				colorObj.style = _color;
			}
		});
		var startColor;
		var endColor;
		this.setColorRange = function(color1, color2) {
			if (zot(color2)) {
				startColor = that.color;
				endColor = color1;
			} else if (zot(color1)) {
				startColor = that.color;
				endColor = color2;
			} else {
				startColor = color1;
				endColor = color2;
			}
			return that;
		}
		var _colorRange = 0;
		Object.defineProperty(that, 'colorRange', {
			get: function() {
				return _colorRange;
			},
			set: function(value) {
				_colorRange = value;
				if (!zot(startColor) && !zot(endColor)) {
					that.color = zim.colorRange(startColor, endColor, value);
				}
			}
		});
		Object.defineProperty(that, 'borderColor', {
			get: function() {
				return _borderColor;
			},
			set: function(value) {
				_borderColor = value;
				if (!borderColorObj) drawShape();
				else borderColorObj.style = _borderColor;
			}
		});
		Object.defineProperty(that, 'borderWidth', {
			get: function() {
				return _borderWidth;
			},
			set: function(value) {
				if (!(value>0)) value = 0;
				_borderWidth = value;
				if (!borderWidthObj || _borderWidth == 0) drawShape();
				else {
					borderWidthObj.width = _borderWidth;
					if (dashed) {
						borderDashedObj.segments = [20, 10];
						borderDashedObj.offset = 5;
					}
				}
			}
		});
		Object.defineProperty(that, 'radius', {
			get: function() {
				return _radius;
			},
			set: function(value) {
				_radius = value;
				drawShape();
			}
		});
		if (style!==false) zimStyleTransforms(this, DS); // global function - would have put on DisplayObject if had access to it

		this.linearGradient = function(colors,ratios,x0,y0,x1,y1) {
			this.linearGradientParams = Array.prototype.slice.call(arguments);
			this.colorCommand.linearGradient(colors,ratios,x0,y0,x1,y1);
			return this;
		}
		this.radialGradient = function(colors,ratios,x0,y0,radius0,x1,y1,radius1) {
			this.radialGradientParams = Array.prototype.slice.call(arguments);
			this.colorCommand.radialGradient(colors,ratios,x0,y0,radius0,x1,y1,radius1);
			return this;
		}

		this.clone = function(exact) {
			var newShape = that.cloneProps(
				exact?new zim.Circle(that.radius, that.color, that.borderColor, that.borderWidth, dashed, percent, strokeObj, style, this.group, inherit)
				:new zim.Circle(oa[0],oa[1],oa[2],oa[3], dashed,oa[4], style, this.group, inherit)
			);
			if (that.linearGradientParams) newShape.linearGradient.apply(newShape, that.linearGradientParams);
			if (that.radialGradientParams) newShape.radialGradient.apply(newShape, that.linearGradientParams);
			return newShape;
		}
	}
	zim.extend(zim.Circle, zim.Container, "clone", "zimContainer", false);
	//-51

/*--
zim.Rectangle = function(width, height, color, borderColor, borderWidth, corner, dashed, strokeObj, style, group, inherit)

Rectangle
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Makes a rectangle shape inside a container.
The registration and origin will be top left corner.
NOTE: mouseChildren is turned to false for all zim Shape containers.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var rect = new Rectangle(200, 100, "blue");
rect.center(stage);

// or with rounded corners:
var rect = new Rectangle({width:200, height:100, color:"blue", corner:20});

// or with 2 pixel white stroke
var rect = new Rectangle(200, 100, "blue", "white", 2);

// fill the rectangle with a Bitmap fill assuming icon has been loaded - not the image property
rect.colorCommand.bitmap(frame.asset("icon.png").image);
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - |ZIM VEE| (default the height if provided else 100) the width
height - |ZIM VEE| (default the width if provided else 100) the height
color - |ZIM VEE| (default "black") the fill color as any CSS color including "rgba()" for alpha fill (set a to 0 for tranparent fill)
borderColor - |ZIM VEE| (default null) the stroke color
borderWidth - |ZIM VEE| (default 1 if stroke is set) the size of the stroke in pixels
corner - (default 0) the round of corner
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
dashed - (default false) set to true for dashed border (if borderWidth or borderColor set)
strokeObj - (default {caps:"butt", joints:"miter", miterLimit:10, ignoreScale:false}) set to adjust stroke properties
	caps options: "butt", "round", "square" or 0,1,2
	joints options: "miter", "round", "bevel" or 0,1,2
	miterLimit is the ration at which the mitered joint will be clipped
	ignoreScale set to true will draw the specified line thickness regardless of object scale
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
linearGradient([colors],[ratios], x0,y0, x1,y1) - shortcut to colorCommand linearGradient method (see properties below)
radialGradient([colors],[ratios], x0,y0,radius0, x1,y1,radius1) - shortcut to colorCommand radialGradient method (see properties below)
setColorRange(color1, color2) - set a color range for shape - used by colorRange property - returns obj for chaining
	if one color is used, the current color is used and color1 is the second color in the range
cache(see Container docs for parameter description) - overrides CreateJS cache() and returns object for chaining
	Leave parameters blank to cache bounds of shape (plus outer edge of border if borderWidth > 0)
hasProp(property as String) - returns true if property exists on object else returns false
clone(exact) - makes a copy of the shape
	exact defaults to false and will use ZIM VEE when cloning - set to true to not use ZIM VEE and make an exact copy
	eg. Tile() could make different dimension shapes each time if ZIM VEE is used for dimension
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
shape - gives access to the rectangle shape
color - get and set the fill color
colorRange - if setColorRange() is used, the colorRange is a ratio (0-1) between the colors
	setting the colorRange will change the color property of the shape
	for instance, shape.setColorRange(blue, pink) then shape.colorRange = .5
	will set the color of the shape to half way between blue and pink
	shape.animate({color:red}, 1000); is a shortcut to animate the colorRange
	shape.wiggle("colorRange", .5, .2, .5, 1000, 5000) will wiggle the colorRange
colorCommand - access to the CreateJS fill command for bitmap, linearGradient and radialGradient fills
	eg. shape.colorCommand.linearGradient([green, blue ,green], [.2, .5, .8], 0, 0, shape.width, 0)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Fill.html
borderColor - get and set the stroke color
borderColorCommand - access to the CreateJS stroke command for bitmap, linearGradient and radialGradient strokes
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Stroke.html
borderWidth - get and set the stroke size in pixels
borderWidthCommand - access to the CreateJS stroke style command (width, caps, joints, miter, ignoreScale)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeStyle.html
borderDashedCommand - access to the CreateJS stroke dashed command (segments, offset)
	see https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeDash.html
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
mouseChildren - set to false so  you do not drag the shape inside the rectangle
	if you nest things inside and want to drag them, will want to set to true
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+52
	zim.Rectangle = function(width, height, color, borderColor, borderWidth, corner, dashed, strokeObj, style, group, inherit) {
		var sig = "width, height, color, borderColor, borderWidth, corner, dashed, strokeObj, style, group, inherit";
		var duo; if (duo = zob(zim.Rectangle, arguments, sig, this)) return duo;
		z_d("52");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Rectangle";
		this.group = group;

	    var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

	   	if (zot(width)) width = DS.width!=null?DS.width:null;
		if (zot(height)) height = DS.height!=null?DS.height:!zot(width)?width:100;
		if (zot(width)) width = height;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:0;
		if (zot(dashed)) dashed = DS.dashed!=null?DS.dashed:false;
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:null;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(color)) color = DS.color!=null?DS.color:(borderWidth>0?"rgba(0,0,0,0)":"black");
		if (zot(strokeObj)) strokeObj = DS.strokeObj!=null?DS.strokeObj:{};

		// PICK
		var oa = remember(width, height, color, borderColor, borderWidth);
		function remember() {return arguments;} // for cloning PICK
		width = zim.Pick.choose(width);
		height = zim.Pick.choose(height);
		color = zim.Pick.choose(color);
		borderColor = zim.Pick.choose(borderColor);
		borderWidth = zim.Pick.choose(borderWidth);

		var that = this;
		var _color = color;
		var _borderColor = borderColor;
		var _borderWidth = borderWidth;
		this.mouseChildren = false;

		var rectangle = this.shape = new createjs.Shape();
		this.addChild(rectangle);

		var g = rectangle.graphics;
		var colorObj;
		var borderColorObj;
		var borderWidthObj;
		drawShape();
		function drawShape() {
			g.c();
			that.colorCommand = colorObj =g.f(_color).command;
			// border of 0 or a string value still draws a border in CreateJS
			if (zot(_borderWidth) || _borderWidth > 0) { // no border specified or a border > 0
				if (!zot(_borderColor) || !zot(_borderWidth)) { // either a border color or thickness
					if (zot(_borderColor)) _borderColor = "black";
					that.borderColorCommand = borderColorObj = g.s(_borderColor).command;
					that.borderWidthCommand = borderWidthObj = g.ss(_borderWidth, strokeObj.caps, strokeObj.joints, strokeObj.miterLimit, strokeObj.ignoreScale).command;
					if (dashed) that.borderDashedCommand = borderDashedObj = g.sd([10, 10], 5).command;
				}
			}
			if (Array.isArray(corner)) {
				g.rc(0,0,width,height,corner[0],corner[1],corner[2],corner[3]);
			} else if (corner > 0) {
				g.rr(0,0,width,height,corner);
			} else {
				g.r(0,0,width,height);
			}

			that.setBounds(0,0,width,height);
		}

		Object.defineProperty(that, 'color', {
			get: function() {
				return _color;
			},
			set: function(value) {
				if (zot(value)) value = "black";
				_color = value;
				colorObj.style = _color;
			}
		});
		var startColor;
		var endColor;
		this.setColorRange = function(color1, color2) {
			if (zot(color2)) {
				startColor = that.color;
				endColor = color1;
			} else if (zot(color1)) {
				startColor = that.color;
				endColor = color2;
			} else {
				startColor = color1;
				endColor = color2;
			}
			return that;
		}
		var _colorRange = 0;
		Object.defineProperty(that, 'colorRange', {
			get: function() {
				return _colorRange;
			},
			set: function(value) {
				_colorRange = value;
				if (!zot(startColor) && !zot(endColor)) {
					that.color = zim.colorRange(startColor, endColor, value);
				}
			}
		});
		Object.defineProperty(that, 'borderColor', {
			get: function() {
				return _borderColor;
			},
			set: function(value) {
				_borderColor = value;
				if (!borderColorObj) drawShape();
				else borderColorObj.style = _borderColor;
			}
		});
		Object.defineProperty(that, 'borderWidth', {
			get: function() {
				return _borderWidth;
			},
			set: function(value) {
				if (!(value>0)) value = 0;
				_borderWidth = value;
				if (!borderWidthObj || _borderWidth == 0) drawShape();
				else {
					borderWidthObj.width = _borderWidth;
					if (dashed) {
						borderDashedObj.segments = [20, 10];
						borderDashedObj.offset = 5;
					}
				}
			}
		});

		this.linearGradient = function(colors,ratios,x0,y0,x1,y1) {
			this.linearGradientParams = Array.prototype.slice.call(arguments);
			this.colorCommand.linearGradient(colors,ratios,x0,y0,x1,y1);
			return this;
		}
		this.radialGradient = function(colors,ratios,x0,y0,radius0,x1,y1,radius1) {
			this.radialGradientParams = Array.prototype.slice.call(arguments);
			this.colorCommand.radialGradient(colors,ratios,x0,y0,radius0,x1,y1,radius1);
			return this;
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function(exact) {
			var newShape =  that.cloneProps(
				exact?new zim.Rectangle(width, height, that.color, that.borderColor, that.borderWidth, corner, dashed, strokeObj, style, this.group, inherit)
				:new zim.Rectangle(oa[0],oa[1],oa[2],oa[3],oa[4], corner, dashed, strokeObj, style, this.group, inherit)
			);
			if (that.linearGradientParams) newShape.linearGradient.apply(newShape, that.linearGradientParams);
			if (that.radialGradientParams) newShape.radialGradient.apply(newShape, that.linearGradientParams);
			return newShape;
		}
	}
	zim.extend(zim.Rectangle, zim.Container, "clone", "zimContainer", false);
	//-52

/*--
zim.Triangle = function(a, b, c, color, borderColor, borderWidth, center, adjust, dashed, strokeObj, style, group, inherit)

Triangle
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Makes a triangle shape inside a container using three line lengths.
Passing one length parameter makes an equilateral triangle.
Passing two length parameters makes an isosceles triangle.
Passing -1 as the last length parameter makes a 90 degree triangle.
NOTE: mouseChildren is turned to false for all zim Shape containers.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var tri = new Triangle(200, null, null, "green");
tri.center(stage);

// all three sides specified - tall pointy triangle with yellow stroke of 10 pixels
var tri = new Triangle(100, 200, 200, "green", "yellow", 10);

// here we adjust so rotation looks better
var tri = new Triangle({a:200, color:"green", adjust:30});
tri.center(stage);
tri.animate({obj:{rotation:360}, time:3000, ease:"linear", loop:true});

// here we fill the triangle with a linear gradient fill
triangle.colorCommand.linearGradient([green, blue ,green], [.2, .5, .8], 0, 0, triangle.width, 0);
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
a, b and c - |ZIM VEE| (default 100) the lengths of the sides
	a will run horizontally along the bottom
	b is upwards and c is back to the origin
	if c is set to -1 will assume a 90 angle
color - |ZIM VEE| (default "black") the fill color as any CSS color including "rgba()" for alpha fill (set a to 0 for tranparent fill)
borderColor - |ZIM VEE| (default null) the stroke color
borderWidth - |ZIM VEE| (default 1 if stroke is set) the size of the stroke in pixels
center - (default true) puts the registration point to the center
adjust - (default 0) pixels to bring center towards vertical base
	the actual center is not really the weighted center
dashed - (default false) set to true for dashed border (if borderWidth or borderColor set)
strokeObj - (default {caps:"butt", joints:"miter", miterLimit:10, ignoreScale:false}) set to adjust stroke properties
	caps options: "butt", "round", "square" or 0,1,2
	joints options: "miter", "round", "bevel" or 0,1,2
	miterLimit is the ration at which the mitered joint will be clipped
	ignoreScale set to true will draw the specified line thickness regardless of object scale
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
linearGradient([colors],[ratios], x0,y0, x1,y1) - shortcut to colorCommand linearGradient method (see properties below)
radialGradient([colors],[ratios], x0,y0,radius0, x1,y1,radius1) - shortcut to colorCommand radialGradient method (see properties below)
setColorRange(color1, color2) - set a color range for shape - used by colorRange property - returns obj for chaining
	if one color is used, the current color is used and color1 is the second color in the range
cache(see Container docs for parameter description) - overrides CreateJS cache() and returns object for chaining
	Leave parameters blank to cache bounds of shape (plus outer edge of border if borderWidth > 0)
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy of the shape
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
shape - gives access to the triangle shape
color - get and set the fill color
colorRange - if setColorRange() is used, the colorRange is a ratio (0-1) between the colors
	setting the colorRange will change the color property of the shape
	for instance, shape.setColorRange(blue, pink) then shape.colorRange = .5
	will set the color of the shape to half way between blue and pink
	shape.animate({color:red}, 1000); is a shortcut to animate the colorRange
	shape.wiggle("colorRange", .5, .2, .5, 1000, 5000) will wiggle the colorRange
colorCommand - access to the CreateJS fill command for bitmap, linearGradient and radialGradient fills
	eg. shape.colorCommand.linearGradient([green, blue ,green], [.2, .5, .8], 0, 0, shape.width, 0)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Fill.html
borderColor - get and set the stroke color
borderColorCommand - access to the CreateJS stroke command for bitmap, linearGradient and radialGradient strokes
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Stroke.html
borderWidth - get and set the stroke size in pixels
borderWidthCommand - access to the CreateJS stroke style command (width, caps, joints, miter, ignoreScale)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeStyle.html
borderDashedCommand - access to the CreateJS stroke dashed command (segments, offset)
	see https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeDash.html
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
one, two, three - read only - points with x, y properties for bottom left, bottom right, top right
angles - read only - Array of angles [bottom left, bottom right, top right]
adjusted - read only - the value of the adjust parameter or 0 if no adjust was supplied
mouseChildren - set to false so  you do not drag the shape inside the triangle
	if you nest things inside and want to drag them, will want to set to true
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+53
	zim.Triangle = function(a, b, c, color, borderColor, borderWidth, center, adjust, dashed, strokeObj, style, group, inherit) {
		var sig = "a, b, c, color, borderColor, borderWidth, center, adjust, dashed, strokeObj, style, group, inherit";
		var duo; if (duo = zob(zim.Triangle, arguments, sig, this)) return duo;
		z_d("53");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Triangle";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(a)) a = DS.a!=null?DS.a:100;
		if (zot(b)) b = DS.b!=null?DS.b:a;
		if (zot(c)) c = DS.c!=null?DS.c:b;
		if (c==-1) c = Math.sqrt(Math.pow(a,2)+Math.pow(b,2));
		if (zot(center)) center = DS.center!=null?DS.center:true;
		if (zot(adjust)) adjust = DS.adjust!=null?DS.adjust:0;
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:null;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(color)) color = DS.color!=null?DS.color:(borderWidth>0?"rgba(0,0,0,0)":"black");
		if (zot(adjust)) adjust = DS.adjust!=null?DS.adjust:0;
		if (zot(strokeObj)) strokeObj = DS.strokeObj!=null?DS.strokeObj:{};

		// PICK
		var oa = remember(a, b, c, color, borderColor, borderWidth);
		function remember() {return arguments;} // for cloning PICK
		a = zim.Pick.choose(a);
		b = zim.Pick.choose(b);
		c = zim.Pick.choose(c);
		this.a = a;
		this.b = b;
		this.c = c;
		color = zim.Pick.choose(color);
		borderColor = zim.Pick.choose(borderColor);
		borderWidth = zim.Pick.choose(borderWidth);

		var that = this;
		var _color = color;
		var _borderColor = borderColor;
		var _borderWidth = borderWidth;
		this.mouseChildren = false;

		var lines = [a,b,c];
		lines.sort(function(a, b){return b-a});
		var aa = lines[0];
		var bb = lines[1];
		var cc = lines[2];
		var order = [lines.indexOf(a), lines.indexOf(b), lines.indexOf(c)];

		if (aa > bb+cc) {
			zog("zim display - Triangle(): invalid triangle lengths");
			return;
		}

		var tri = this.shape = new createjs.Shape();
		this.adjusted = adjust;
		this.addChild(tri);

		var g = tri.graphics;
		var colorObj;
		var borderColorObj;
		var borderWidthObj;
		drawShape();
		function drawShape() {
			g.c();
			that.colorCommand = colorObj =g.f(_color).command;
			// border of 0 or a string value still draws a border in CreateJS
			if (zot(_borderWidth) || _borderWidth > 0) { // no border specified or a border > 0
				if (!zot(_borderColor) || !zot(_borderWidth)) { // either a border color or thickness
					if (zot(_borderColor)) _borderColor = "black";
					that.borderColorCommand = borderColorObj = g.s(_borderColor).command;
					that.borderWidthCommand = borderWidthObj = g.ss(_borderWidth, strokeObj.caps, strokeObj.joints, strokeObj.miterLimit, strokeObj.ignoreScale).command;
					if (dashed) that.borderDashedCommand = borderDashedObj = g.sd([10, 10], 5).command;
				}
			}
			g.mt(0,0);
			that.one={x:0,y:0};
			g.lt(a,0);
			that.two={x:a,y:0};

			// find biggest angle with cosine rule
			var angle1 = Math.acos( (Math.pow(bb,2) + Math.pow(cc,2) - Math.pow(aa,2)) / (2 * bb * cc) ) * 180 / Math.PI;

			// use the sine rule for next biggest angle
			var angle2 = Math.asin( bb * Math.sin(angle1 * Math.PI / 180) / aa ) * 180 / Math.PI;

			// find last angle
			var angle3 = 180 - angle1 - angle2;

			// get position of angles by mapping to opposite side sizes
			// as in smallest angle is across from smallest side
			// largest angle is across from largest size, etc.
			var temp = [angle1, angle2, angle3]; // largets to smallest
			that.angles = [temp[order[1]], temp[order[2]], temp[order[0]]];

			var nextAngle = that.angles[1];
			var backX = Math.cos(nextAngle * Math.PI / 180) * b;
			var upY = Math.sin(nextAngle * Math.PI / 180) * b;

			var width = Math.max(a, a-backX);
			var height = upY
			that.setBounds(0,adjust,width,height);
			tri.y = height;

			g.lt(a-backX,0-upY);
			that.three={x:a-backX,y:0-upY};
			g.cp();

			if (center) {
				that.regX = width/2;
				that.regY = height/2;
			}
			if (adjust) {
				that.shape.y+=adjust;
			}
		}

		Object.defineProperty(that, 'color', {
			get: function() {
				return _color;
			},
			set: function(value) {
				if (zot(value)) value = "black";
				_color = value;
				colorObj.style = _color;
			}
		});
		var startColor;
		var endColor;
		this.setColorRange = function(color1, color2) {
			if (zot(color2)) {
				startColor = that.color;
				endColor = color1;
			} else if (zot(color1)) {
				startColor = that.color;
				endColor = color2;
			} else {
				startColor = color1;
				endColor = color2;
			}
			return that;
		}
		var _colorRange = 0;
		Object.defineProperty(that, 'colorRange', {
			get: function() {
				return _colorRange;
			},
			set: function(value) {
				_colorRange = value;
				if (!zot(startColor) && !zot(endColor)) {
					that.color = zim.colorRange(startColor, endColor, value);
				}
			}
		});
		Object.defineProperty(that, 'borderColor', {
			get: function() {
				return _borderColor;
			},
			set: function(value) {
				_borderColor = value;
				if (!borderColorObj) drawShape();
				else borderColorObj.style = _borderColor;
			}
		});
		Object.defineProperty(that, 'borderWidth', {
			get: function() {
				return _borderWidth;
			},
			set: function(value) {
				if (!(value>0)) value = 0;
				_borderWidth = value;
				if (!borderWidthObj || _borderWidth == 0) drawShape();
				else {
					borderWidthObj.width = _borderWidth;
					if (dashed) {
						borderDashedObj.segments = [20, 10];
						borderDashedObj.offset = 5;
					}
				}
			}
		});

		this.linearGradient = function(colors,ratios,x0,y0,x1,y1) {
			this.linearGradientParams = Array.prototype.slice.call(arguments);
			this.colorCommand.linearGradient(colors,ratios,x0,y0,x1,y1);
			return this;
		}
		this.radialGradient = function(colors,ratios,x0,y0,radius0,x1,y1,radius1) {
			this.radialGradientParams = Array.prototype.slice.call(arguments);
			this.colorCommand.radialGradient(colors,ratios,x0,y0,radius0,x1,y1,radius1);
			return this;
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function(exact) {
			var newShape = that.cloneProps(
				exact?new zim.Triangle(a, b, c, that.color, that.borderColor, that.borderWidth, center, adjust, dashed, strokeObj, style, this.group, inherit)
				:new zim.Triangle(oa[0],oa[1],oa[2],oa[3],oa[4],oa[5], center, adjust, dashed, strokeObj, style, this.group, inherit)
			);
			if (that.linearGradientParams) newShape.linearGradient.apply(newShape, that.linearGradientParams);
			if (that.radialGradientParams) newShape.radialGradient.apply(newShape, that.linearGradientParams);
			return newShape;
		}
	}
	zim.extend(zim.Triangle, zim.Container, "clone", "zimContainer");
	//-53

/*--
zim.Squiggle = function(color, thickness, points, length, controlLength, controlType, lockControlType, showControls, lockControls, handleSize, allowToggle, move, dashed, onTop, stickColor, selectColor, selectPoints, editPoints, interactive, strokeObj, style, group, inherit)

Squiggle
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Makes a squiggly line with a number of points.
The points have Bezier controls - little handles that change the shape of the line.
The type of control can be specified overall and individually - and can be hidden or shown
The type of control can be changed by double clicking the point - colors of the handles will change
Points can be added by clicking on the line or removed by SHIFT clicking a point.
CTRL Z will undo adding or removing a point
The shape of the line can be recorded with the recordData() method and recreated with the setData() method
The Squiggle is set by default to show and hide controls when clicked
It is also draggable by default when the controls are showing
It can be set to copy with a shift click

MULTIPLE SELECT
Multiple points can be selected and dragged or moved with the keyboard arrows (moves 10 pixels with shift key down)

NOTE: mouseChildren is turned to false for all zim Shape containers.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var squiggle = new Squiggle().center(); // makes a line with default 4 points with Bezier controls
var line = new Squiggle({points:2, controlType:"none"}).pos(100,100); // makes a diagonal straight line that is editable
END EXAMPLE

EXAMPLE
// Animate along a Squiggle
// see https://zimjs.com/explore/squiggleAnimate.html for more
var line = new Squiggle().center();
new Circle(10, red).addTo().animate({path:line}, 1000);
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
color - (default green) the line color as any CSS color including "rgba()" for alpha
thickness - (default 2) the thickness of the line in pixels
points - (default 4)a number of points to start with to make the shape
	OR an SVG path like: points:"M0,129.5c22,0,40-31,40-41c0-8-3.2-13-10-13" etc. (also see SVGContainer)
	OR an array of points as follows:
	[[controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType], [etc]]
	controlX and controlY - the x and y location of the control Container which holds the point circle and the two control rectangles
	rect1X, rect1Y, rect2X, rect2Y - (default based on controlLength) the x and y location of the control rectangles relative to the control location
	circleX and circleY - (default 0) the x and y location of the circle relative to the control location (usually 0, 0)
	controlType - (default main controlType parameter or "straight" if not controlType parameter) the point's controlType "none", "mirror", "straight" or "free"
length - (default 300) the default length of line used to create the squiggle (also specifies the squiggle's bounds(0, 0, length, thickness))
controlLength - |ZIM VEE| (default radius*numPoints/4) specify a Number to override the calculated default
controlType - (default "straight") one of four String values as follows:
	none - there are no control rectangles (they are actually set at 0,0).  This makes a corner at the circle point.
	mirror - the control rectangles reflect one another about the point circle - lengths are kept even
	straight - the control rectangles keep a straight line through the point circle but length is independent
	free - the control rectangle moves independently from the other control rectangle
	** The controlType can be specified for each point - see the points parameter
	** The controlType can be changed by doubleClicking the point circle to cycle through the controls in the order above - unless the lockControlType is set to true
lockControlType - (default false) set to true to disable doubleClicking of point circles to change controlType
showControls - (default true) set to false to start with controls not showing - can change this after with controlsVisible property or showControls() and hideControls() methods
lockControls - (default false) set to true to lock the editing of controls - can't move the points or handles - but can see them if showControls is set to true
handleSize - (default 20 mobile 10 for non-mobile) the size of control boxes and affects the circles too proportionally
allowToggle - (default true) set false to let turn off clicks showing and hiding controls
move - (default true) set to false to disable dragging when controls are showing
	can also set to "always" to allow movement when controls are not showing
dashed - (default false) set to true for dashed border (if borderWidth or borderColor set)
onTop - (default true) set to false to not bring shape to top of container when dragging
stickColor - (default "#111") set the stick color of the controls
selectColor - (default white) the color of the selected circle or rectangle of the controls if selectPoints is true
selectPoints - (default true) set to false to not allow point controls to be selected for keyboard control
editPoints - (default true) lets user add points by pressing on shape path.
	set to "anywhere" to let users add points anywhere - will add points with controlType:"none"
	set to false to not allow adding or removing points with click or shift click
interactive - (default true) set to false to turn off controls, move, toggle, select, edit - leaving just the shape
strokeObj - (default {caps:"butt", joints:"miter", miterLimit:10, ignoreScale:false}) set to adjust stroke properties
	caps options: "butt", "round", "square" or 0,1,2
	joints options: "miter", "round", "bevel" or 0,1,2
	miterLimit is the ration at which the mitered joint will be clipped
	ignoreScale set to true will draw the specified line thickness regardless of object scale
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
addPoint(percent, controlType) - add a point at a percent (100) of the total curve
	this is handy to make path have the same number of points for animate() path tweens
	controlType can be as specified in main points parameter
	returns object for chaining
addPoints(num, controlType, startPoint, spread, dataOnly, points) - add num points between existing points
	controlType can be as specified in main points parameter
	specify a startPoint to add points between the startPoint and the next point (one segment of points)
	spread (default false) set to true to spread points evenly around path rather than evenly between segments
	dataOnly and points are used internally
	returns object for chaining
interpolate(num, startPoint, spread) - get point data {x,y} for this existing points and num (default 1) points inbetween
	used with hitTestPath
	specify a startPoint to get points between the startPoint and the next point (one segment of points)
	spread (default false) set to true to spread points evenly around path rather than evenly between segments
	returns an array of point objects with x, y properties and an r property for ratio of distance along path
recordData(toJSON) - returns an object with x, y, points, color, borderColor, borderWidth, move, toggle, controls PROPERTIES to be used with setData() method
	if toJSON (default false) is set to true, the return value is a JSON string
	the points data comes from the points property
setData(data, fromJSON) - sets the properties to match the data object passed in - this should come from recordData()
	if fromJSON (default false) is set to true, it will assume a JSON string is passed in as data
	the points data is parsed with the set setPoints() so the number of points should be the same
	returns object for chaining
recordPoints(popup) - returns an array with the same format as the points parameter - or can just use points property
	popup - (default false) set to true to open a zim Pane (squiggle.pane) with the points in a zim TextArea (squiggle.textArea) (click off to close)
	NOTE: the TextArea output uses JSON.stringify() - to add the points to the points parameter of the Squiggle use JSON.parse(output);
	NOTE: using zog(JSON.stringify(squiggle.recordData()))... the console will remove the quotes from the controlTypes so those would have to be manually put back in before parse() will work
setPoints(data) - sets the Squiggle points to the data from recordPoints
	This does not remake the Squiggle but rather shifts the controls so the number of points should be the same
changeControl(index, type, rect1X, rect1Y, rect2X, rect2Y, circleX, circleY, update) - change a control type and properties at an index
	accepts ZIM DUO normal parameters or configuration object literal with parameter names as propterties
	passing in null as the index will change all points to the specified properties
	the update parameter defaults to false so set to true to show update or call update() below
	this is so multiple changes can be batched before calling update - for instance when animating blobs.
transformPoints(transformType, amount, x, y) - scale, rotate, move points without affecting controls or borderWidth - returns object for chaining
	Note - does not adjust original Bounds
	transformType - String any of: "scale", "scaleX", "scaleY", "rotation", "x", "y"
	amount - the amount to transform
	x, y - (default 0, 0) the x and y position to transform about
update(normalize) - update the Squiggle if animating control points, etc. would do this in a Ticker
	set normalize (default false) to true to use pointsAdjusted for rotated and scaled points
	and if animating along the path after setting rotation or scale on point
	just leave out if only animating points
showControls() - shows the controls (and returns squiggle) also see controlsVisible property
hideControls() - hides the controls (and returns squiggle) also see controlsVisible property
toggle(state - default null) - shows controls if hidden and hides controls if showing (returns the object for chaining)
	or pass in true to show controls or false to hide controls
traverse(obj, start, end, time) - animates obj from start point to end point along path - thanks KV for the thought!
	set start point greater than end point to traverse backwards
	will dispatch a "traversed" event when done
setColorRange(color1, color2) - set a color range for shape - used by colorRange property - returns obj for chaining
	if one color is used, the current color is used and color1 is the second color in the range
getPointAngle(index) - gets the angle made by the tangent at the index provided
getSegmentPoint(point1, point2) - returns an array of [point1, controlPoint1, controlPoint2, point2]
	used internally for animating to path and adding removing Bezier points
getAdjacentSegmentData(index) - returns an array of two arrays:
	The first is an array of cubic Bezier points for segments adjacent and including the provided point index
	each element is in the form of [point1, controlPoint1, controlPoint2, point2]
	The second is an array of starting point indexes for the segments that were tested
	used internally to drag an animation along the path
getCurvePoint(ratio, segmentRatios, segmentPoints) gets a point along whole curve at the ratio (0-1) provided
	along with x and y values, the point has a z value that is the index of the squiggle point before the calculated point
	ratio is 0-1 with 0 being at the first point and 1 being at the end of the last segment
	segmentRatios and segmentPoints will be calculated if not provided
	used internally for animating along the path - if lockControls is true, only animate will precalculate these values
linearGradient([colors],[ratios], x0,y0, x1,y1) - shortcut to thicknessCommand linearGradient method (see properties below)
radialGradient([colors],[ratios], x0,y0,radius0, x1,y1,radius1) - shortcut to thicknessCommand radialGradient method (see properties below)
cache(see Container docs for parameter description) - overrides CreateJS cache() and returns object for chaining
	Leave parameters blank to cache bounds of shape (plus outer edge of border if borderWidth > 0)
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy of the shape
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
shape - gives access to the shape of the squiggle
color - get and set the fill color
colorRange - if setColorRange() is used, the colorRange is a ratio (0-1) between the colors
	setting the colorRange will change the color property of the shape
	for instance, shape.setColorRange(blue, pink) then shape.colorRange = .5
	will set the color of the shape to half way between blue and pink
	shape.animate({color:red}, 1000); is a shortcut to animate the colorRange
	shape.wiggle("colorRange", .5, .2, .5, 1000, 5000) will wiggle the colorRange
stickColor - get or set the stickColor - requires an update() to see changes
colorCommand - access to the CreateJS fill command for bitmap, linearGradient and radialGradient fills
	eg. shape.colorCommand.linearGradient([green, blue ,green], [.2, .5, .8], 0, 0, shape.width, 0)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Fill.html
thickness - get and set the stroke size in pixels
thicknessCommand - access to the CreateJS stroke style command (width, caps, joints, miter, ignoreScale)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeStyle.html
dashedCommand - access to the CreateJS stroke dashed command (segments, offset)
	see https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeDash.html
num - get the number of points - to set, use the points property
points - get or set the points array of the Squiggle in the same format as the points parameter:
	[[controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType], [etc]]
pointsAdjusted - get points with any point rotation converted to 0 - see update(true)
pointControls - get an array of controls (a container) - use this to animate controls
pointCircles - get an array of control circles - use this to place some other object at the point
pointObjects - get an array of point objects for each point in the following format:
	[[control, circle, rect1, rect2, controlType], [etc.]]
	control - the container for the control that holds the circle and rectangles (also see pointControls)
	circle - the control point circle (also see pointCircles)
	rect1 - the first control point rectangle
	rect2 - the second control point rectangle
	controlType - the control type: default is "straight" (or null) and there is also "mirror", "free" and "none"
	NOTE: control, circle, rect1, rect2 can be positioned or animated and controlType can be changed
	NOTE: the update() method must be called if manually changing the control positions or type
	NOTE: if constantly animating the controls then use a Ticker.add(function(){squiggle.update();})
	NOTE: also see recordData(), setData(), recordPoints(), setPoints() methods for further options
addPointFactor - (default 20) used when placing new points along edge (editPoints is true)
	divides the distance between points by this amount - the smaller the more accurate but also slower
addMinDistance - (default 15) edge press needs to be within this distance to add a point to the edge
segmentPoints - a read-only array of cubic Bezier points for each segment
	each element is in the form of [point1, controlPoint1, controlPoint2, point2]
	used internally to animate to the path and add and remove Bezier points
segmentRatios - a read-only array of cumulative ratio lengths of segments
	for instance the default five points (four segments) is [.25, .5, .75, 1]
	used internally to animate to the path and attribute proportional time to each segment
controls - access to the container that holds the sets of controls
	each control is given a read-only num property
sticks - access to the container that holds the control sticks
lastSelected - access to the last selected (or created) control container
lastSelectedIndex - the index number of the last selected controls
controlsVisible - get or set the visibility of the controls - or use showControls() and hideControls()
toggled - read-only Boolean property as to whether picker is showing
types - get or set the general array for the types ["mirror", "straight", "free", "none"]
	changing this or removing a type will adjust the order when the user double clicks the points to change their type
	this is not an array of types for each point - see the points property to access the types of each point
lockControls - Boolean to lock controls from being adjusted or not
lockControlType - Boolean to lock the type of the controls in their current state or not
allowToggle - Boolean to get or set clicking to show and hide controls
move - Boolean to drag or not drag Squiggle when controls are showing
	can also set to "always" to allow movement when controls are not showing
onTop - get or set the onTop
selectPoints - get or set whether points can be selected
interactive - get or set whether the shape is interactive - toggle, move, change or add controls, etc.
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
keyFocus - get or set the keyboard focus on the DisplayObject - see also zim.KEYFOCUS
   will be set to true if this DisplayObject is the first made or DisplayObject is the last to be used with keyboard
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "change" event for when the bezier controls are adjusted (pressup only)
	if monitoring constant change is needed add a pressmove event to Squiggle.controls
	the change event object has a transformType property with values of "move", "bezierPoint", "bezierHandle", "bezierSwitch"
dispatches "controlsshow" and "controlshide" events when clicked off and on and toggle is true
dispatches an "update" event if the points are changed or a point is added or removed
	this removes all listeners on the old shape and controls
	so any custom listeners on shape and controls will need to be re-applied - use the update event to do so
dispatches a "traversed" event when traverse() is done - the event object has an obj property for the traversing object
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover

MORE
https://zimjs.com/squiggle
https://www.youtube.com/watch?v=BA1bGBU4itI&list=PLCIzupgRt1pYtMlYPtNTKCtztFBeOtyc0
Note the points property has been split into points and pointObjects (and there have been a few property changes) since the time of the video
--*///+53.2
	zim.Squiggle = function(color, thickness, points, length, controlLength, controlType, lockControlType, showControls, lockControls, handleSize, allowToggle, move, dashed, onTop, stickColor, selectColor, selectPoints, editPoints, interactive, strokeObj, style, group, inherit) {
		var sig = "color, thickness, points, length, controlLength, controlType, lockControlType, showControls, lockControls, handleSize, allowToggle, move, dashed, onTop, stickColor, selectColor, selectPoints, editPoints, interactive, strokeObj, style, group, inherit";
		var duo; if (duo = zob(zim.Squiggle, arguments, sig, this)) return duo;
		z_d("53.2");

		this.group = group;
		var DS = style===false?{}:zim.getStyle("Squiggle", this.group, inherit);

		if (zot(thickness)) thickness = DS.thickness!=null?DS.thickness:6;
		if (zot(length)) length = DS.length!=null?DS.length:300;
		if (zot(points)) points = DS.points!=null?DS.points:5;
		if (typeof points == "string") {
			var svgProcessor = new zim.SVGContainer();
			points = svgProcessor.processPath(points);
		}
		var num = typeof points == "number" ? points : points.length;
		if (num == 0) return;
		if (zot(controlLength)) controlLength = DS.controlLength!=null?DS.controlLength:(length / num);
		this.zimContainer_constructor(length, controlLength, null, null, false);
		this.type = "Squiggle";

		if (zot(dashed)) dashed = DS.dashed!=null?DS.dashed:false;
		if (zot(color)) color = DS.color!=null?DS.color:zim.blue;
		if (color.style) {this.colorCommand = color; color = "black";}

		if (zot(controlType)) controlType = DS.controlType!=null?DS.controlType:null;
		var originalControlType = controlType;
		if (zot(controlType)) controlType = "mirror";
		if (zot(lockControlType)) lockControlType = DS.lockControlType!=null?DS.lockControlType:false;
		if (zot(interactive)) interactive = DS.interactive!=null?DS.interactive:true;
		if (zot(showControls)) showControls = DS.showControls!=null?DS.showControls:interactive;
		var _controls = showControls;
		if (zot(lockControls)) lockControls = DS.lockControls!=null?DS.lockControls:!interactive;
		if (zot(handleSize)) handleSize = DS.handleSize!=null?DS.handleSize:zim.mobile()?20:10;
		if (zot(allowToggle)) allowToggle = DS.allowToggle!=null?DS.allowToggle:interactive;
		if (zot(move)) move = DS.move!=null?DS.move:interactive;
		if (zot(stickColor)) stickColor = DS.stickColor!=null?DS.stickColor:"#111";
		if (zot(selectColor)) selectColor = DS.selectColor!=null?DS.selectColor:"#fff";
		if (zot(selectPoints)) selectPoints = DS.selectPoints!=null?DS.selectPoints:interactive;
		this.stickColor = stickColor;

		if (zot(onTop)) onTop = DS.onTop!=null?DS.onTop:true;
		if (zot(editPoints)) editPoints = DS.editPoints!=null?DS.editPoints:interactive;
		if (zot(strokeObj)) strokeObj = DS.strokeObj!=null?DS.strokeObj:{};

		var that = this;
		var types = this.types = ["mirror", "straight", "free", "none"];
		this.interactive = interactive;
		this.num = num;
		this.onTop = onTop;
		this.move = move;
		this.editPoints = editPoints;
		this.allowToggle = allowToggle;
		this.lockControlType = lockControlType;
		this.selectPoints = selectPoints;
		this.lockControls = lockControls;

		var _points;
		var _pointCircles;
		var _pointControls;
		var _color = color;
		var _thickness = thickness;
		var colorObj;
		var thicknessObj;
		var borderDashedObj;

		var shape;
		var moveDownEvent;
		var movePressEvent;
		var moveUpEvent;
		var stage;

		var draggingCheck = that.move;
		var min = 2; // min distance within which a click will add a point

		var mapMove;
		var drawShape;
		var sets;

		if (originalControlType && typeof points != "number") {
			// override controlType
			loop(points, function(point) {
				point[8]=originalControlType;
				if (originalControlType == "none") {
					point[4]=point[5]=point[6]=point[7]=0;
				}
			});
		}

		init();
		function init() {
			if (sets) sets.removeAllEventListeners();

			that.num = num = typeof points == "number" ? points : points.length;
			num = Math.max(2,num);
			controlLength = length / num;

			shape = that.shape = new zim.Shape({style:false}).addTo(that);
			var sticks = that.sticks = new zim.Shape({style:false}).addTo(that);
			if (handleSize <= 0) sticks.removeFrom();
			var g = shape.graphics;
			g.c();
			var s = sticks.graphics;
			s.c();

			var ballS = handleSize/10*8;
			var rectS = handleSize;

			if (that.selectPoints) {
				that.selectedBalls = new zim.SelectionSet();
				that.selectedRect1s = new zim.SelectionSet();
				that.selectedRect2s = new zim.SelectionSet();

				that.selectionManager = new zim.SelectionManager([
					that.selectedBalls,
					that.selectedRect1s,
					that.selectedRect2s
				], "ctrl", false);
			} else {
				that.selectionManager = new zim.SelectionManager(null, "ctrl");
			}

			var mobile = zim.mobile();

			sets = that.controls = new zim.Container({style:false}).addTo(that); // sets - a set contains a ball and two rects
			if (that.interactive) sets.drag({onTop:!mobile});
			_points = [];
			_pointControls = [];
			_pointCircles = [];

			var angle, point, temp, set, rect1, rect2, ball, type, setInfo;

			for (var i=0; i<num; i++) {
				set = new zim.Container({style:false}).addTo(sets);
				set.num = i;
				if (typeof points == "number") { // no sets yet
					var stickLength = zim.Pick.choose(controlLength);
					temp = new zim.Container(stickLength, thickness, null, null, false).addTo(that).loc({x:i*length/(num-1)-stickLength/2, y:i%2*stickLength});
					ball = new zim.Circle(ballS, that.selectPoints&&that.selectedBalls.isSelected(i)?selectColor:zim.light, zim.dark, 2, null, null, null, false)
						.centerReg(temp)
						.loc({x:stickLength/2, y:0});
					rect1 = new zim.Rectangle(rectS, rectS, that.selectPoints&&that.selectedRect1s.isSelected(i)?selectColor:getBackgroundColor(controlType),  handleSize==0?null:zim.dark,  handleSize==0?null:2, null, null, null, false)
						.centerReg(temp)
						.loc({x:0,y:0});
					rect2 = new zim.Rectangle(rectS, rectS, that.selectPoints&&that.selectedRect2s.isSelected(i)?selectColor:getBackgroundColor(controlType),  handleSize==0?null:zim.dark,  handleSize==0?null:2, null, null, null, false)
						.centerReg(temp)
						.loc({x:stickLength,y:0});

					var ballPoint = temp.localToLocal(ball.x, ball.y, sets);
					ball.x = ballPoint.x;
					ball.y = ballPoint.y;
					ball.addTo(set, null, false);
					var rect1Point = temp.localToLocal(rect1.x, rect1.y, sets);
					rect1.x = controlType=="none"?0:rect1Point.x-ball.x;
					rect1.y = controlType=="none"?0:rect1Point.y-ball.y;
					rect1.addTo(set, null, false);
					var rect2Point = temp.localToLocal(rect2.x, rect2.y, sets);
					rect2.x = controlType=="none"?0:rect2Point.x-ball.x;
					rect2.y = controlType=="none"?0:rect2Point.y-ball.y;
					rect2.addTo(set, null, false);
					set.x = ball.x;
					set.y = ball.y;
					ball.x = 0;
					ball.y = 0;
					if (controlType=="none") ball.addTo(set, null, false); // on top

				} else { // passing in set data

					// _pointCircles are relative to squiggle but handles are relative to ball
					// points is an array of [[setX, setY, ballX, ballY, handleX, handleY, handle2X, handle2Y, type], etc.]

					setInfo = points[i];
					type = setInfo[8] ? setInfo[8] : controlType;
					set.loc({x:setInfo[0], y:setInfo[1]});
					ball = new zim.Circle(ballS, zim.light, zim.dark, 2, null, null, null, false)
						.centerReg(set)
						.loc({x:setInfo[2],y:setInfo[3]});
					rect1 = new zim.Rectangle(rectS, rectS, getBackgroundColor(type),  handleSize==0?null:zim.dark,  handleSize==0?null:2, null, null, null, false)
						.centerReg(set, 0)
						.loc({x:setInfo[4],y:setInfo[5]});
					rect2 = new zim.Rectangle(rectS, rectS, getBackgroundColor(type), handleSize==0?null:zim.dark,  handleSize==0?null:2, null, null, null, false)
						.centerReg(set, 0)
						.loc({x:setInfo[6],y:setInfo[7]});
				}

				ball.mySet = set;
				ball.rect1 = rect1;
				ball.rect2 = rect2;
				ball.index = i;

				if (mobile) {
					ball.on("mousedown", mobileDouble);
				} else {
					ball.on("dblclick", doubleIt);
				}

				rect1.ball = ball;
				rect1.other = rect2;
				rect2.ball = ball;
				rect2.other = rect1;

				if (handleSize==0) {
					ball.expand(10);
					rect1.expand(10);
					rect2.expand(10);
				}

				if (mobile) {
					ball.expand();
					rect1.expand();
					rect2.expand();
				}

				point = [set, ball, rect1, rect2, setInfo?setInfo[8]:controlType];
				_points.push(point);
				_pointCircles.push(ball);
				_pointControls.push(set);
			}

			var tappedTwice = false;
			function mobileDouble(e) {
				if (!tappedTwice) {
					tappedTwice = true;
					setTimeout(function() {
						tappedTwice = false;
					}, 300);
				} else {
					e.preventDefault();
					doubleIt(e);
				}
			}

			function doubleIt(e) {
				if (that.lockControlType) return;
				var ball = e.target;
				// cycle through the types
				var type = _points[ball.index][4] ? _points[ball.index][4] : controlType;
				if (Math.abs(ball.rect1.x) <= 2 && Math.abs(ball.rect1.y) <= 2 && Math.abs(ball.rect2.x) <= 2 && Math.abs(ball.rect2.y) <= 2) {
					type = "none"
				}
				if (type == "none") {
					ball.parent.addChildAt(ball, 0);
				}
				// modulus going backwards needs to add the length so it does not go negative
				type = that.types[(that.types.indexOf(type)+(that.shiftKey?-1:1)+that.types.length)%that.types.length];
				if (type == "none") {
					ball.rect1.x =  ball.rect1.y =  ball.rect2.x =  ball.rect2.y = 0;
					ball.parent.addChild(ball);
					e.stopImmediatePropagation();
				}
				_points[ball.index][4] = type;

				ball.rect1.color = getBackgroundColor(type);
				ball.rect2.color = getBackgroundColor(type);
				drawShape();
				var ev = new createjs.Event("change");
				ev.controlType = "bezierSwitch";
				that.dispatchEvent(ev);
				ball.stage.update();
			};

			function getBackgroundColor(type) {
				var colors = {straight:zim.pink, free:zim.yellow, none:zim.blue};
				return colors[type] ? colors[type] : zim.purple;
			}

			drawShape = function () {

				g.c();

				var minThickness = zim.mobile() ? 10 : 6;
				if (thickness < minThickness) {
					// 1. draw backing grab line
					g.s("rgba(0,0,0,.01)").ss(minThickness);
					var set = _points[0][0];
					var ballPoint = set.localToLocal(_points[0][1].x, _points[0][1].y, shape);
					g.mt(ballPoint.x, ballPoint.y);

					var currentIndex; var nextIndex;
					for (var i=0; i<_points.length; i++) {
						var currentIndex = i;
						var nextIndex = (i+1)%_points.length;

						var set = _points[currentIndex][0];
						var ball = _points[currentIndex][1];
						var control1 = _points[currentIndex][2];
						var control2 = _points[currentIndex][3];

						var nextSet = _points[nextIndex][0];
						var nextBall = _points[nextIndex][1];
						var nextControl1 = _points[nextIndex][2];
						var nextControl2 = _points[nextIndex][3];

						var control2Point = set.localToLocal(control2.x, control2.y, shape);
						var nextControl1Point = nextSet.localToLocal(nextControl1.x, nextControl1.y, shape);
						var nextBallPoint = nextSet.localToLocal(nextBall.x, nextBall.y, shape);

						if (i != _points.length-1) {
							g.bt(
								control2Point.x, control2Point.y,
								nextControl1Point.x, nextControl1Point.y,
								nextBallPoint.x, nextBallPoint.y
							);
						}
					}
				}

				// 2. draw real line

				if (!that.colorCommand) that.colorCommand = colorObj = g.s(_color).command;
				if (!that.thicknessCommand) that.thicknessCommand = thicknessObj = g.ss(_thickness, strokeObj.caps, strokeObj.joints, strokeObj.miterLimit, strokeObj.ignoreScale).command;
				if (dashed) {
					if (!that.dashedCommand) that.dashedCommand = borderDashedObj = g.sd([10, 10], 5).command;
				}

				set = _points[0][0];
				ballPoint = set.localToLocal(_points[0][1].x, _points[0][1].y, shape);
				g.mt(ballPoint.x, ballPoint.y);

				s.c().s(that.stickColor).ss(1);

				var currentIndex; var nextIndex;
				for (var i=0; i<_points.length; i++) {
					var currentIndex = i;
					var nextIndex = (i+1)%_points.length;

					var set = _points[currentIndex][0];
					var ball = _points[currentIndex][1];
					var control1 = _points[currentIndex][2];
					var control2 = _points[currentIndex][3];

					var nextSet = _points[nextIndex][0];
					var nextBall = _points[nextIndex][1];
					var nextControl1 = _points[nextIndex][2];
					var nextControl2 = _points[nextIndex][3];

					var control2Point = set.localToLocal(control2.x, control2.y, shape);
					var nextControl1Point = nextSet.localToLocal(nextControl1.x, nextControl1.y, shape);
					var nextBallPoint = nextSet.localToLocal(nextBall.x, nextBall.y, shape);

					if (i != _points.length-1) {
						g.bt(
							control2Point.x, control2Point.y,
							nextControl1Point.x, nextControl1Point.y,
							nextBallPoint.x, nextBallPoint.y
						);
					}

					// create the sticks
					var ballPoint = set.localToLocal(ball.x, ball.y, shape);
					var control1Point = set.localToLocal(control1.x, control1.y, shape);

					if (i == 0) control1.visible = 0;
					if (i != 0) s.mt(ballPoint.x, ballPoint.y).lt(control1Point.x, control1Point.y);
					if (i != _points.length-1) s.mt(ballPoint.x, ballPoint.y).lt(control2Point.x, control2Point.y);
					if (i == _points.length-1) control2.visible = 0;
				}

				if (dashed) g.append(that.dashedCommand);
				g.append(that.thicknessCommand);
				g.append(that.colorCommand);
			}
			drawShape();

			var startPosition;
			sets.on("mousedown", function(e) {
				stage = e.target.stage;
				if (that.lockControls) return;
				if (that.selectPoints) that.keyFocus = true;
				startPosition = {x:e.target.x, y:e.target.y};
				if (e.target.rect1) { // then mousedown on ball - which has a rect1
					var ball = e.target;
					ball.startX = ball.x;
					ball.startY = ball.y;
					ball.rect1.startX = ball.rect1.x;
					ball.rect1.startY = ball.rect1.y;
					ball.rect2.startX = ball.rect2.x;
					ball.rect2.startY = ball.rect2.y;
				} else { // mousedown on control
					var rect = e.target;
					rect.startX = rect.x;
					rect.startY = rect.y;
					var ball = rect.ball;
					var index = ball.index;
					var type = controlType;
					if (!zot(_points[index][4])) type = _points[index][4];
					if (type == "straight") {
						var other = rect.other;
						var dX = other.x - ball.x;
						var dY = other.y - ball.y;
						other.stickLength = Math.sqrt(Math.pow(dX,2) + Math.pow(dY,2));
					}
				}
				if (that.selectPoints) {
					// need to reset all start points for each control circle and rectangle moved
					var currentSet = that.selectionManager.currentSet;
					if (currentSet && currentSet.selections && currentSet.selections.length > 0) {
						for(var i=0; i<currentSet.selections.length; i++) {
							var point = that.pointObjects[currentSet.selections[i]];
							point[1].startX = point[1].x;
							point[1].startY = point[1].y;
							point[2].startX = point[2].x;
							point[2].startY = point[2].y;
							point[3].startX = point[3].x;
							point[3].startY = point[3].y;
						}
					}
				}
			});

			if (that.selectPoints) {
				sets.tap(function (e) {
					if (e.target.rect1) { // then mousedown on ball - which has a rect1
						var ball = e.target;
						that.selectedBalls.toggle(ball.parent.num);
					} else { // mousedown on control
						var rect = e.target;
						rect.color = "white";
						var ball = rect.ball;
						if (ball.rect1 == rect) that.selectedRect1s.toggle(ball.parent.num);
						else that.selectedRect2s.toggle(ball.parent.num);
					}
					// loop through all controls and set to right color based on selection
					for (var i=0; i<that.pointObjects.length; i++) {
						var po = that.pointObjects[i];
						po[1].color = that.selectedBalls.isSelected(i)?zim.white:zim.light;
						po[2].color = that.selectedRect1s.isSelected(i)?zim.white:getBackgroundColor(po[4]);
						po[3].color = that.selectedRect2s.isSelected(i)?zim.white:getBackgroundColor(po[4]);
					}
					stage.update();
				});
			}

			sets.on("pressmove", function(e) {
				if (that.lockControls) return;
				if (that.selectPoints) {
					var currentSelected = getCurrentSelected();
					if (currentSelected.indexOf(e.target) == -1) {
						mapMove(e.target);
						drawShape();
					} else {
						if (currentSelected.length > 0) {
							diffX = e.target.x-e.target.startX;
							diffY = e.target.y-e.target.startY;
							for(var i=0; i<currentSelected.length; i++) {
								var pointObj = currentSelected[i];
								pointObj.x = pointObj.startX + diffX;
								pointObj.y = pointObj.startY + diffY;
								mapMove(pointObj);
							}
							drawShape();
						}
					}
				} else {
					mapMove(e.target);
					drawShape();
				}
			});

			sets.on("pressup", function(e) {
				if (that.lockControls) return;
				var moveControlCheck = (e.target.x != startPosition.x || e.target.y != startPosition.y);
				var ev = new createjs.Event("change");
				if (e.target.rect1) { // pressup on ball
					ev.controlType = "bezierPoint";
					endMove(e.target);
				} else {
					ev.controlType = "bezierHandle";
				}
				if (moveControlCheck) that.dispatchEvent(ev);
			});

			function endMove(target) {
				if (that.selectPoints) {
					var currentSelected = getCurrentSelected();
					if (currentSelected && currentSelected.indexOf(target) == -1) {
						replaceControls(target);
					} else if (currentSelected && currentSelected.length>0) {
						for(var i=0; i<currentSelected.length; i++) {
							replaceControls(currentSelected[i]);
						}
					} else {
						replaceControls(target);
					}
				} else {
					replaceControls(target);
				}
			};

			that.changeControl = function(index, type, rect1X, rect1Y, rect2X, rect2Y, circleX, circleY, update) {
				var sig = "index, type, rect1X, rect1Y, rect2X, rect2Y, circleX, circleY, update";
				var duo; if (duo = zob(that.changeControl, arguments, sig)) return duo;
				if (zot(index)) {
					for (var i=0; i<_points.length; i++) {
						that.changeControl(i, type, rect1X, rect1Y, rect2X, rect2Y, circleX, circleY);
					}
					return;
				}
				var point = _points[index];
				point[4] = type;
				if (type == "none") {
					if (!zot(circleX)) point[2].x = circleX;
					if (!zot(circleY)) point[2].y = circleY;
					point[2].x = point[1].x;
					point[2].y = point[1].y;
					point[3].x = point[1].x;
					point[3].y = point[1].y;
					point[1].parent.addChild(point[1]);
				} else {
					if (!zot(rect1X)) point[2].x = rect1X;
					if (!zot(rect1Y)) point[2].y = rect1Y;
					if (!zot(rect2X)) point[3].x = rect2X;
					if (!zot(rect2Y)) point[3].y = rect2Y;
					if (!zot(circleX)) point[1].x = circleX;
					if (!zot(circleY)) point[1].y = circleY;
					point[1].parent.addChildAt(point[1], 0);
				}
				if (update) {
					that.update();
					if (that.stage) that.stage.update();
				}
			}

			that.transformPoints = function(transformType, amount, x, y) {
				that.points = zim.transformPoints(that.points, transformType, amount, x, y);
				return that;
			}

			that.traverse = function(obj, start, end, time) {
				var ratios = zim.copy(that.segmentRatios);
				ratios.unshift(0);
				if (zot(end)) end = start+1;
				var forward = start < end;
				if (forward) {
					var startPercent = ratios[start]*100;
					var endPercent = ratios[end]*100;
				} else {
					var startPercent = 50 + (100 - ratios[start]*100)/2;
					var endPercent = 50 + (100 - ratios[end]*100)/2;
				}
				obj.percentComplete = startPercent;
				obj.animate({
					ease:"linear",
					props:{path:that},
					rewind:!forward,
					time:time,
					events:true
				});
				obj.on("animation", function (e) {
					// when it hits the end it may start over
					if (obj.percentComplete > endPercent || obj.percentComplete == 0) {
						obj.stopAnimate();
						e.remove();
						var eventObj = new createjs.Event("traversed");
						eventObj.obj = obj;
						that.dispatchEvent(eventObj);
					}
				});
				return that;
			}


			that.update = function(normalize) {
				if (normalize) {
					// located any rotated or scaled points
					// and set them back to non-rotated and non-scaled
					// but keep control handles at the earlier positions
					// need to normalize before doing more manual updates with Beziers
					// do not need to normalize if animating blob points
					that.points = that.pointsAdjusted;
				} else {
					drawShape();
				}
				return that;
			}

			if (that.interactive) {
				if (move) shape.drag({onTop:false});
				moveDownEvent = shape.on("mousedown", function(e) {
					stage = e.target.stage;
					startPosition = {x:shape.x, y:shape.y};
					if (that.selectPoints) that.keyFocus = true;
					upTop();
				});
				movePressEvent = shape.on("pressmove", function() {
					sets.x = shape.x;
					sets.y = shape.y;
					sticks.x = shape.x;
					sticks.y = shape.y;
				});
				moveUpEvent = shape.on("pressup", function() {
					var moveControlCheck = (shape.x != startPosition.x || shape.y != startPosition.y);
					var movePoint = shape.localToLocal(0,0,that.parent);
					that.x = movePoint.x;
					that.y = movePoint.y;
					sets.x = sets.y = sticks.x = sticks.y = shape.x = shape.y = 0;
					if (moveControlCheck) {
						var ev = new createjs.Event("change");
						ev.controlType = "move";
						that.dispatchEvent(ev);
					}
					stage.update();
				});

				if (!that.move) stopDragging(true); // true is first time
			}

			function upTop() {
				if (that.onTop) {
					var nc = that.parent.numChildren-1;
					if (that.parent.getChildAt(nc).type == "Keyboard") nc--;
					that.parent.setChildIndex(that, nc);
				}
			}

			that.toggleEvent = that.on("mousedown", function() {
				if (!that.allowToggle) return;
				if (!_controls) {
					that.showControls();
					that.dispatchEvent("controlsshow");
				}
			});

			that.added(function() {
				that.toggleStageEvent = that.stage.on("stagemousedown", function(e) {
					if (!that.allowToggle || !that.stage) return;
					if (_controls && !that.hitTestPoint(e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY, false)) {
						that.hideControls();
						that.dispatchEvent("controlshide");
					}
				});
			});

			that.clickEvent = that.on("click", function() {
				if (that.ctrlKey) {
					setTimeout(function() { // give time for record to work if drag with ctrl down
						that.clone(true).addTo(that.stage).mov(0, 100);
						if (that.allowToggle) {
							that.hideControls();
							that.dispatchEvent("controlshide");
						}
						var ev = new createjs.Event("change");
						ev.controlType = "move";
						that.dispatchEvent(ev);
						that.stage.update();
					}, 50);
				}
			});

			that.hideControls = function() {
				that.toggled = false;
				sets.visible = false;
				sticks.visible = false;
				_controls = false;
				if (that.stage) that.stage.update();
				if (!that.allowToggle && that.move) stopDragging();
				return that;
			}
			if (!showControls) that.hideControls();
			that.showControls = function() {
				that.toggled = true;
				// if call this with code then will not trigger a change event - not good for TransformManager.persist()
				sets.visible = true;
				sticks.visible = true;
				_controls = true;
				sets.x = shape.x;
				sets.y = shape.y;
				sticks.x = shape.x;
				sticks.y = shape.y;
				that.addChildAt(shape,0); // put to bottom incase dragged
				if (that.move && !that.allowToggle) startDragging();
				if (that.stage) that.stage.update();
				return that;
			}

			that.toggle = function(state) {
				if (state===true) that.showControls();
				else if (state===false) that.hideControls();
				else if (_controls) that.hideControls();
				else that.showControls();
				return that;
			}

			that.recordData = function(toJSON) {
				if (zot(toJSON)) toJSON = false;
				var obj = {
					type:"Blob",
					index:that.parent?that.parent.getChildIndex(that):-1,
					x:that.x, y:that.y,
					points:that.recordPoints(),
					color:that.color,
					thickness:that.thickness,
					move:that.move,
					toggle:that.allowToggle,
					controlsVisible:_controls
				}
				if (toJSON) return JSON.stringify(obj);
				return obj;
			}

			that.setData = function(data, fromJSON) {
				if (zot(data)) return;
				if (fromJSON) {
					try{
					  data = JSON.parse(data);
					} catch(e) {
					  return;
				  	}
				}
				var index = data.index;
				if (zot(index)) index = -1;
				delete data.index;

				var pointData = data.points;
				if (!zot(pointData)) that.setPoints(pointData);
				delete data.points;
				that.num = pointData.length;

				for (var d in data) {
					that[d] = data[d];
				}
				if (that.parent) {
					that.parent.setChildIndex(that, index);
				}
				that.update();
				return that;
			}

			that.recordPoints = function(popup) {
				// balls are relative to blob but handles are relative to ball
				// points is an array of [[ballX, ballY, handleX, handleY, handle2X, handle2Y, type], etc.]
				if (zot(popup)) popup = false;
				var points = that.points;
				if (popup) {
					if (!that.pane) {
						var pane = that.pane = new zim.Pane({
							container:that.stage,
							width:Math.min(500, that.stage.width-20),
							height:Math.min(500, that.stage.height-20),
							draggable:true,
						});
						var textArea = that.textArea = new zim.TextArea(Math.min(400, that.stage.width-70), Math.min(400, that.stage.height-70));
						textArea.centerReg(pane);
					}
					that.textArea.text = JSON.stringify(points);
					that.pane.show();
				}
				return points;
			}

			that.setPoints = function(points) {
				// adjust blob to match points passed in from recordPoints
				var p;
				var p2;
				for (var i=0; i<points.length; i++) {
					p = _points[i];
					p2 = points[i];
					if (zot(p)) continue;
					p[0].x = p2[0];
					p[0].y = p2[1];
					p[1].x = p2[2];
					p[1].y = p2[3];
					p[2].x = p2[4];
					p[2].y = p2[5];
					p[3].x = p2[6];
					p[3].y = p2[7];
					p[4] = p2[8];
				}
				that.update();
				return that;
			}
			if (style!==false) zimStyleTransforms(that, DS);
			that.clone = function(commands) {
				var color = commands?that.colorCommand:that.color;
				var color = commands?that.colorCommand:that.color;
				var newShape =  that.cloneProps(new zim.Squiggle(commands?that.colorCommand:that.color, that.thickness, that.recordPoints(), length, controlLength, controlType, lockControlType, sets.visible, lockControls, handleSize, that.allowToggle, that.move, dashed, onTop, stickColor, selectColor, selectPoints, that.editPoints, interactive, strokeObj, style, that.group, inherit));
				if (that.linearGradientParams) newShape.linearGradient.apply(newShape, that.linearGradientParams);
				if (that.radialGradientParams) newShape.radialGradient.apply(newShape, that.linearGradientParams);
				return newShape;
			}

			// to add a control - make sure click in one spot - not drag
			that.shape.on("mousedown", function (e) {
				stage = e.target.stage;
				if (!that.editPoints) return;
				if (that.controlsVisible) {
					that.pressX = e.stageX/stage.scaleX;
					that.pressY = e.stageY/stage.scaleX;
				} else {
					that.pressX = null;
					that.pressY = null;
				}
			});
			that.addPointFactor = 20;
			that.addMinDistance = 15;
			that.shape.on("pressup", function (e) {
				if (!that.editPoints) return;
				if (that.pressX && Math.abs(that.pressX-e.stageX/stage.scaleX) < min && Math.abs(that.pressY-e.stageY/stage.scaleY) < min) {
					if (that.selectPoints) that.lastPoints = zim.copy(that.points);
					var points = that.points;
					var point = that.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
					var pointBefore = zim.closestPointAlongCurve(point, that.segmentPoints);

					if (that.editPoints == "anywhere") {
						points.splice(pointBefore+1, 0, [point.x, point.y, 0,0, 0,0, 0,0]);
						that.points = points;
						that.changeControl({index:pointBefore+1, type:"mirror", update:true});
					} else { // only on edge
						// test close enough to edge otherwise return
						var p = that.pointsAdjusted;
						var cubic = that.getSegmentPoint(p[pointBefore], p[(pointBefore+1)%p.length]);
						var length = zim.distanceAlongCurve(cubic);
						var testNum = Math.round(length/that.addPointFactor);
						var testPoints = that.interpolate(testNum, pointBefore, points);
						var closest=10000;
						var closestPoint;
						var closestIndex;

						zim.loop(testPoints, function (p, k) {
							if (k==0) return; // don't put on existing point
							var d = zim.dist(p, point);
							if (d < closest) {
								closest = d;
								closestIndex = k;
								closestPoint = p;
							}
						});
						if (closest < that.addMinDistance) {
							var ratios = that.segmentRatios;
							var currentRatio = ratios[pointBefore];
							var lastRatio = pointBefore>0?ratios[pointBefore-1]:0;
							that.addPoint(100*(lastRatio+(currentRatio-lastRatio)*(closestIndex/testNum)));
						}
					}
					that.lastSelectedIndex = pointBefore+1;
					that.lastSelected = that.controls.getChildAt(that.lastSelectedIndex);
					that.stage.update();
				}
			});

			// remove point
			that.controls.on("click", function (e) {
				that.lastSelected = e.target.parent;
				var index = that.lastSelectedIndex = that.controls.getChildIndex(e.target.parent);
				if (!that.editPoints) return;
				if (that.selectionManager.shiftKey) { // remove
					// if (that.selectionManager.currentSet == that.selectedBalls && that.selectedBalls.selections.length > 0) return;
					if (e.target.type == "Circle") {
						if (that.controls.numChildren <= 2) return;
						var points = that.points;
						if (that.selectPoints) that.lastPoints = zim.copy(points);
						points.splice(index, 1); // remove the point at the index
						that.points = points;
						that.stage.update();
						that.lastSelected = that.lastSelectedIndex = null;
					}
				}
			});

			if (!_controls) that.hideControls();
			that.dispatchEvent("update");
		} // end of init()

		// if (that.selectPoints) {
		function getCurrentSelected() {
			var answer = [];
			var currentSet = that.selectionManager.currentSet;
			if (currentSet && currentSet.selections && currentSet.selections.length > 0) {
				for(var i=0; i<currentSet.selections.length; i++) {
					var point = that.pointObjects[currentSet.selections[i]];
					if (currentSet == that.selectedBalls) {
						answer.push(point[1]);
					} else if (currentSet == that.selectedRect1s) {
						answer.push(point[2]);
					}	else if (currentSet == that.selectedRect2s) {
						answer.push(point[3]);
					} else {
						continue;
					}
				}
			}
			return answer;
		}


		function replaceControls(target) {
			// move ball back to origin and move set accordingly
			// so if we animate the set it will behave as expected
			if (target.type != "Circle") return;
			var ball = target;
			var set = ball.mySet;
			var rect1 = ball.rect1;
			var rect2 = ball.rect2;
			rect1.x -= ball.x;
			rect1.y -= ball.y;
			rect2.x -= ball.x;
			rect2.y -= ball.y;
			set.x += ball.x;
			set.y += ball.y;
			ball.x = 0;
			ball.y = 0;
		}

		that.selectionManager.on("keydown", function (e) {
			if (!that.selectPoints) return;
			if (!that.keyFocus) return;
			if (e.keyCode >= 37 && e.keyCode <= 40) {
				var currentSelected = getCurrentSelected();
				if (currentSelected.length > 0) {
					for(var i=0; i<currentSelected.length; i++) {
						var pointObj = currentSelected[i];
						if (e.keyCode == 37) pointObj.x -= that.selectionManager.shiftKey?10:1;
						else if (e.keyCode == 39) pointObj.x += that.selectionManager.shiftKey?10:1;
						else if (e.keyCode == 38) pointObj.y -= that.selectionManager.shiftKey?10:1;
						else if (e.keyCode == 40) pointObj.y += that.selectionManager.shiftKey?10:1;
						mapMove(pointObj);
					}
					drawShape();
					if (that.stage) that.stage.update();
				}
			}
		});

		that.selectionManager.on("keyup", function (e) {
			if (!that.selectPoints) return;
			if (!that.keyFocus) return;
			if (e.keyCode >= 37 && e.keyCode <= 40) {
				var currentSelected = getCurrentSelected();
				if (currentSelected.length > 0) {
					for(var i=0; i<currentSelected.length; i++) {
						replaceControls(currentSelected[i]);
					}
				}
			}
		});

		that.selectionManager.on("undo", function () {
			if (!that.selectPoints) return;
			if (!that.keyFocus) return;
			if (that.lastPoints) {
				var tempPoints = zim.copy(that.lastPoints);
				that.lastPoints = zim.copy(that.points);
				that.points = tempPoints;
				if (that.stage) that.stage.update()
			}
		});
		// }


		mapMove = function (target) {
			if (that.lockControls) return;
			if (target.rect1) { // pressmove on ball
				var ball = target;
				var diffX = ball.x - ball.startX;
				var diffY = ball.y - ball.startY;
				ball.rect1.x = ball.rect1.startX + diffX;
				ball.rect1.y = ball.rect1.startY + diffY;
				ball.rect2.x = ball.rect2.startX + diffX;
				ball.rect2.y = ball.rect2.startY + diffY;
			} else { // pressmove on control
				var rect = target;
				var other = rect.other; // the other handle
				var ball = rect.ball;
				var index = ball.index;
				var type = controlType;
				if (!zot(_points[index][4])) type = _points[index][4];
				if (type == "straight" || type == "mirror") {
					var dX = rect.x - ball.x;
					var dY = rect.y - ball.y;
					if (type == "mirror") {
						other.x = ball.x - dX;
						other.y = ball.y - dY;
					} else {
						var a = Math.atan2(dY, dX);
						var dNewX = -other.stickLength * Math.cos(a+Math.PI);
						var dNewY = -other.stickLength * Math.sin(a+Math.PI);
						other.x = ball.x - dNewX;
						other.y = ball.y - dNewY;
					}
				}
			}

			// that.setBounds();
			// decided not to dynamically set bounds
			// they really just go around the control points and not the shape
			// and if dynamically set they go to null if the control points are not showing
		};

		Object.defineProperty(that, 'move', {
			get: function() {
				return move;
			},
			set: function(value) {
				if (move != value) {
					move = value;
					if (value) {
						startDragging();
					} else {
						stopDragging();
					}
				}
			}
		});

		Object.defineProperty(that, 'interactive', {
			get: function() {
				return interactive;
			},
			set: function(value) {
				interactive = value;
				that.showControls = interactive;
				that.allowToggle = interactive;
				that.editPoints = interactive;
				that.lockControls = !interactive; // note negative!
				that.selectPoints = interactive;
				that.move = interactive;
				that.points = that.points; // force remake
			}
		});

		Object.defineProperty(that, 'allowToggle', {
			get: function() {
				return allowToggle;
			},
			set: function(value) {
				if (allowToggle != value) {
					allowToggle = value;
					if (allowToggle) {
						if (that.move) startDragging();
					} else {
						if (!_controls && that.move) stopDragging();
					}
				}
			}
		});

		function startDragging() {
			if (that.move=="always") return;
			if (draggingCheck) return;
			draggingCheck = true;
			shape.drag({onTop:false});
			moveDownEvent = shape.on("mousedown", moveDownEvent);
			movePressEvent = shape.on("pressmove", movePressEvent);
			moveUpEvent = shape.on("pressup", moveUpEvent);
		}
		function stopDragging(making) {
			if (that.move=="always") return;
			if (!making && !draggingCheck) return;
			draggingCheck = false;
			shape.noDrag();
			shape.off("mousedown", moveDownEvent);
			shape.off("pressmove", movePressEvent);
			shape.off("pressup", moveUpEvent);
		}

		var _lockControls = lockControls;
		Object.defineProperty(that, 'lockControls', {
			get: function() {
				return _lockControls;
			},
			set: function(value) {
				_lockControls = value;
				if (value) {
					that.controls.mouseChildren = false;
					that.controls.mouseEnabled = false;
				} else {
					that.controls.mouseChildren = true;
					that.controls.mouseEnabled = true;
				}
			}
		});
		that.lockControls = _lockControls;

		Object.defineProperty(that, 'controlsVisible', {
			get: function() {
				return _controls;
			},
			set: function(value) {
				_controls = value;
				if (value) {
					that.showControls();
				} else {
					that.hideControls();
				}
			}
		});

		Object.defineProperty(that, 'color', {
			get: function() {
				return _color;
			},
			set: function(value) {
				if (zot(value)) value = "black";
				_color = value;
				colorObj.style = _color;
			}
		});
		var startColor;
		var endColor;
		this.setColorRange = function(color1, color2) {
			if (zot(color2)) {
				startColor = that.color;
				endColor = color1;
			} else if (zot(color1)) {
				startColor = that.color;
				endColor = color2;
			} else {
				startColor = color1;
				endColor = color2;
			}
			return that;
		}
		var _colorRange = 0;
		Object.defineProperty(that, 'colorRange', {
			get: function() {
				return _colorRange;
			},
			set: function(value) {
				_colorRange = value;
				if (!zot(startColor) && !zot(endColor)) {
					that.color = zim.colorRange(startColor, endColor, value);
				}
			}
		});

		Object.defineProperty(that, 'thickness', {
			get: function() {
				return _thickness;
			},
			set: function(value) {
				if (!(value>0)) value = 0;
				_thickness = value;
				if (!thicknessObj || _thickness == 0) drawShape();
				else {
					thicknessObj.width = _thickness;
					if (dashed) {
						borderDashedObj.segments = [20, 10];
						borderDashedObj.offset = 5;
					}
				}
			}
		});

		if (typeof KEYFOCUS !== typeof undefined) zim.KEYFOCUS = KEYFOCUS;
		Object.defineProperty(this, 'keyFocus', {
			get: function() {
				return zim.KEYFOCUS == that;
			},
			set: function(value) {
				zim.KEYFOCUS = that;
			}
		});
		if (!zim.KEYFOCUS) setFocus();
		function setFocus() {if (!that.selectPoints) return; that.keyFocus = true; var d=document.activeElement; if (d) d.blur();}


		Object.defineProperty(that, 'points', {
			get: function() {
				var points = [];
				var point; var p;
				for (var i=0; i<_points.length; i++) {
					p = _points[i];
					point = [
						zim.decimals(p[0].x),
						zim.decimals(p[0].y),
						zim.decimals(p[1].x),
						zim.decimals(p[1].y),
						zim.decimals(p[2].x),
						zim.decimals(p[2].y),
						zim.decimals(p[3].x),
						zim.decimals(p[3].y)
					];
					if (p[4] && p[4]!=="mirror") point.push(p[4])
					points.push(point);
				}
				return points;
			},
			set: function(value) {
				that.dispose(true);
				points = value;

				if (that.shape) {
					that.shape.graphics.clear();
					that.sticks.graphics.clear();
					that.controls.noDrag();
					that.removeAllChildren();
					delete that.shape;
					delete that.sticks;
					delete that.controls;
				}
				init(); // remake Squiggle
				that.lockControls = _lockControls;
 			}
		});

		Object.defineProperty(that, 'pointsAdjusted', { // points with rotation
			get: function() {
				var points = [];
				var point; var p; var poo;
				var pObjects = that.pointObjects;
				zim.loop(pObjects.length, function(i, t) {
					po = pObjects[i];
					p = _points[i];
					if (po[0].rotation==0 && po[0].scaleX==0 && po[0].scaleY==0) { // get points
						point = [
							zim.decimals(p[0].x),
							zim.decimals(p[0].y),
							zim.decimals(p[1].x),
							zim.decimals(p[1].y),
							zim.decimals(p[2].x),
							zim.decimals(p[2].y),
							zim.decimals(p[3].x),
							zim.decimals(p[3].y)
						];
					} else {
						var lo1 = po[0].localToLocal(po[2].x, po[2].y, po[0].parent);
						var lo2 = po[0].localToLocal(po[3].x, po[3].y, po[0].parent);
						point = [
							zim.decimals(p[0].x),
							zim.decimals(p[0].y),
							zim.decimals(p[1].x),
							zim.decimals(p[1].y),
							zim.decimals(lo1.x-p[0].x),
							zim.decimals(lo1.y-p[0].y),
							zim.decimals(lo2.x-p[0].x),
							zim.decimals(lo2.y-p[0].y)
						];
					}
					if (p[4] && p[4]!=="mirror") point.push(p[4])
					points.push(point);
				});
				return points;
			},
			set: function(value) {
				if (zon) zog("Squiggle() - pointsAdjusted is read only")
 			}
		});

		Object.defineProperty(that, 'pointObjects', {
			get: function() {
				return _points;
			},
			set: function(value) {
				if (zon) {zog("Squiggle() - pointObjects is read only - but its contents can be manipulated - use squiggle.update() after changes")}
 			}
		});

		Object.defineProperty(that, 'pointControls', {
			get: function() {
				return _pointControls;
			},
			set: function(value) {
				if (zon) {zog("Squiggle() - pointControls is read only - but its contents can be manipulated - use blob.update() after changes")}
			}
		});

		Object.defineProperty(that, 'pointCircles', {
			get: function() {
				return _pointCircles;
			},
			set: function(value) {
				if (zon) {zog("Squiggle() - pointCircles is read only - but its contents can be manipulated - use blob.update() after changes")}
			}
		});

		// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// path manipulation and animating to path methods

		Object.defineProperty(that, 'segmentPoints', {
			get: function() {
				var array = []; // array of prepared segment points
				var p = that.pointsAdjusted;
				zim.loop(p.length-1, function(i, t) {
					var s = that.getSegmentPoint(p[i], p[i+1])
					array.push(s);
				});
				return array;
			},
			set: function(value) {
				if (zon) {zog("Squiggle() - segmentPoints is read only")}
			}
		});

		Object.defineProperty(that, 'segmentRatios', {
			get: function() {
				var distances = []
				var total = 0;
				zim.loop(that.segmentPoints, function(points) {
					var d = zim.distanceAlongCurve(points)
					distances.push(d);
					total += d;
				});
				var percents = [];
				var totalPercents = 0;
				zim.loop(distances, function (d) {
					var p = d/total;
					totalPercents += p;
					percents.push(totalPercents);
				});
				return percents;
			},
			set: function(value) {
				if (zon) {zog("Squiggle() - segmentRatios is read only")}
			}
		});

		that.getPointAngle = function(index) {
			var p = that.pointObjects[index][0]; // parent
			var r1 = that.pointObjects[index][2];
			var r2 = that.pointObjects[index][3];
			if (p==that.stage) {
				var globalR1 = new zim.Point(r1.x, r1.y);
				var globalR2 = new zim.Point(r2.x, r2.y);
			} else {
				var globalR1 = p.localToGlobal(r1.x, r1.y);
				var globalR2 = p.localToGlobal(r2.x, r2.y);
			}
			return zim.angle(globalR1.x, globalR1.y,globalR2.x, globalR2.y);
		}

		that.getSegmentPoint = function(point1, point2) {
			if (zot(point1) || zot(point2)) return;
			// dragging points temporarily puts data out of order
			if (point1[2] != 0 || point1[3] != 0) {
				point1[4] -= point1[2];
				point1[5] -= point1[3];
				point1[6] -= point1[2];
				point1[7] -= point1[3];
				point1[0] += point1[2];
				point1[1] += point1[3];
				point1[2] = 0;
				point1[3] = 0;
			}
			if (point2[2] != 0 || point2[3] != 0) {
				point2[4] -= point2[2];
				point2[5] -= point2[3];
				point2[6] -= point2[2];
				point2[7] -= point2[3];
				point2[0] += point2[2];
				point2[1] += point2[3];
				point2[2] = 0;
				point2[3] = 0;
			}
			var p1 = {x:point1[0], y:point1[1]};
			var p2 = {x:point1[0]+point1[6], y:point1[1]+point1[7]};
			var p3 = {x:point2[0]+point2[4], y:point2[1]+point2[5]};
			var p4 = {x:point2[0], y:point2[1]};
			if (sets.x != 0 || sets.y !=0) {
				p1.x+=sets.x;
				p2.x+=sets.x;
				p3.x+=sets.x;
				p4.x+=sets.x;
				p1.y+=sets.y;
				p2.y+=sets.y;
				p3.y+=sets.y;
				p4.y+=sets.y;
			}
			return [p1,p2,p3,p4];
		}

		that.getAdjacentSegmentData = function(index) {
			if (zot(index)) index = 0;
			var p = that.pointsAdjusted;
			if (that.num == 2) {
				return [
					[that.getSegmentPoint(p[0], p[1])],
					[0]
				]
			}
			if (index == 0) {
				return [
					[that.getSegmentPoint(p[0], p[1]),
					that.getSegmentPoint(p[1], p[2])],
					[0,1]
				];
			} else if (index >= that.num-2) {
				return [
					[that.getSegmentPoint(p[that.num-3], p[that.num-2]),
					that.getSegmentPoint(p[that.num-2], p[that.num-1])],
					[that.num-3, that.num-2]
				];
			} else {
				return [
					[that.getSegmentPoint(p[index-1], p[index]),
					that.getSegmentPoint(p[index], p[index+1]),
					that.getSegmentPoint(p[index+1], p[index+2])],
					[index-1,index,index+1]
				];
			}
		}

		that.getCurvePoint = function(ratio, segmentRatios, segmentPoints, getAngle) {
			if (zot(ratio) || isNaN(ratio)) ratio = 0;
			if (zot(segmentRatios)) segmentRatios = that.segmentRatios;
			if (zot(segmentPoints)) segmentPoints = that.segmentPoints;
			if (zot(getAngle)) getAngle = false;
			var percents = segmentRatios;
			var segments = segmentPoints;
			var afterIndex = zim.loop(percents, function (p, i) {
				if (p >= ratio) return i;
			});
			var earlierPercent = afterIndex > 0 ? percents[afterIndex-1] : 0;
			var localTotal = afterIndex > 0 ? (percents[afterIndex]-percents[afterIndex-1]):percents[afterIndex];
			if (!localTotal) return undefined;
			var localPercent = (ratio-earlierPercent)/localTotal;
			var finalPoint = zim.pointAlongCurve(segments[afterIndex], localPercent, getAngle);
			var finalFinalPoint = that.localToGlobal(finalPoint.x, finalPoint.y);
			finalFinalPoint.angle = finalPoint.angle;
			finalFinalPoint.z = afterIndex;
			return !zot(finalFinalPoint) ? finalFinalPoint : undefined;
		}

		function proportion(p1, p2, ratio) {
			return {
				x:p1.x + (p2.x-p1.x)*ratio,
				y:p1.y + (p2.y-p1.y)*ratio
			}
		}
		function insertPointData(points, controls, ratios, percent, controlType, skipPoint, dataOnly) {
			var index = points.length-1; // adjust for squiggle
			var lastRatio = 0;
			var currentRatio = 0;
			if (percent == 100 && that.type == "Squiggle") percent = 99.99;
			percent = (percent+100000)%100;

			zim.loop(ratios, function (ratio, i) {
				if (percent/100 < ratio) {
					index = i;
					currentRatio = ratio;
					return true;
				}
				lastRatio = ratio;
			});
			var segment = that.segmentPoints[index];
			var r = currentRatio > 0?(percent/100-lastRatio)/(currentRatio-lastRatio):0
			// zog(percent)
			// zog(percent/100-currentRatio/ratios.length)
			// var r = currentRatio > 0?(percent/100-1/ratios.length*currentRatio)/(currentRatio-lastRatio):0

			var point = zim.pointAlongCurve(segment, r)
			var newPoint = [point.x,point.y, 0, 0];
			if (skipPoint) return;
			if (dataOnly) {
				that.interpolatedPoints.push({x:point.x, y:point.y, r:percent/100});
				return;
			}
			if (controlType != "none") {
				// calculate new handles and adjust old handles
				// [controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType]
				var startHandle = proportion(segment[0], segment[1], r);
				var midPoint = proportion(segment[1], segment[2], r);
				var endHandle = proportion(segment[2], segment[3], r);
				var newStartHandle = proportion(startHandle, midPoint, r);
				var newEndHandle = proportion(midPoint, endHandle, r);
				newPoint[4] = newStartHandle.x-point.x
				newPoint[5] = newStartHandle.y-point.y;
				newPoint[6] = newEndHandle.x-point.x;
				newPoint[7] = newEndHandle.y-point.y;
				var start = that.localToLocal(startHandle.x, startHandle.y, controls[index])
				points[index][6] = start.x;
				points[index][7] = start.y;
				var end = that.localToLocal(endHandle.x, endHandle.y, controls[(index+1)%points.length])
				points[(index+1)%points.length][4] = end.x;
				points[(index+1)%points.length][5] = end.y;
			}
			if (controlType) newPoint[8] = controlType;
			points.splice(index+1, 0, newPoint);
		}

		this.addPoint = function(percent, controlType) {
			if (zot(percent)) percent = 100;
			var points = that.points;
			var ratios = that.segmentRatios;
			var controls = that.pointControls;
			insertPointData(points, controls, ratios, percent, controlType);
			that.points = points;
			that.num = points.length;
			return that;
		}

		this.addPoints = function(num, controlType, startPoint, spread, dataOnly, points) {
			if (zot(points)) points = zim.copy(that.points);
			var ratios = zim.copy(that.segmentRatios);
			var lastRatio = 0;

			if (dataOnly) that.interpolatedPoints = [];

			// dataOnly should add points to current point too
			// but can't just use current point because sometimes that is static
			// like when dragging the shape or a point - it does not register until mouseup
			// and things like hitTestPath need that to be dynamic
			// So the below does not work:
			// if (dataOnly) {
			// 	that.interpolatedPoints = [];
			// 	zim.loop(points, function (point, i) {
			// 		if (!zot(startPoint) && i!=startPoint) return;
			// 		that.interpolatedPoints.push({x:point[0], y:point[1]})
			// 	});
			// }
			if (spread) var totalPoints = ratios.length*num;
			zim.loop(ratios, function (ratio, j) {
				if (dataOnly) insertPointData(points, that.pointControls, that.segmentRatios, lastRatio*100, controlType, !zot(startPoint) && j!=startPoint, dataOnly);
				var numCount = spread?Math.round(totalPoints*(ratio-lastRatio)):num
				var div = 1/(numCount+1);
				zim.loop(numCount, function(i) {
					var r = lastRatio + (ratio-lastRatio)*div*(i+1);
					insertPointData(points, that.pointControls, that.segmentRatios, r*100, controlType, !zot(startPoint) && j!=startPoint, dataOnly);
					if (!dataOnly && num > 0) that.points = points;
				});
				lastRatio = ratio;
			});
			if (dataOnly && that.type == "Squiggle") insertPointData(points, that.pointControls, that.segmentRatios, 100, controlType, null, dataOnly);
			if (that.stage) that.stage.update();
			that.num = points.length;
			return that;
		}
		this.interpolate = function(num, startPoint, spread, points) {
			if (zot(num)) num = 1;
			// dataOnly will add Point to start point too
			that.addPoints(num, "none", startPoint, spread, true, points)
			return that.interpolatedPoints;
		}

		this.linearGradient = function(colors,ratios,x0,y0,x1,y1) {
			this.linearGradientParams = Array.prototype.slice.call(arguments);
			this.thicknessCommand.linearGradient(colors,ratios,x0,y0,x1,y1);
			return this;
		}
		this.radialGradient = function(colors,ratios,x0,y0,radius0,x1,y1,radius1) {
			this.radialGradientParams = Array.prototype.slice.call(arguments);
			this.thicknessCommand.radialGradient(colors,ratios,x0,y0,radius0,x1,y1,radius1);
			return this;
		}

		this.dispose = function(temp) {
			// if (that.toggleStageEvent) that.stage.off("stagemousedown", that.toggleStageEvent);
			// this.zimContainer_dispose();
			// return true;
			if (!that.shape) return;
			that.shape.cursor = "default";
			for (var i=0; i<that.points.length; i++) {
				that.pointObjects[i][1].removeAllEventListeners();
			}
			for (i=0; i<_pointCircles.length; i++) {
				_pointCircles[i].removeAllEventListeners();
			}
			that.sticks.removeFrom(that);
			that.controls.removeFrom(that);
			that.shape.removeAllEventListeners();
			that.controls.removeAllEventListeners();
			that.off("mousedown", that.toggleEvent);
			that.off("click", that.clickEvent);
			if (that.toggleStageEvent) that.stage.off("stagemousedown", that.toggleStageEvent);
			if (!temp && that.selectPoints) that.selectionManager.removeAllEventListeners();
			return
		}
	}
	zim.extend(zim.Squiggle, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-53.2


/*--
zim.Blob = function(color, borderColor, borderWidth, points, radius, controlLength, controlType, lockControlType, showControls, lockControls, handleSize, allowToggle, move, dashed, onTop, stickColor, selectColor, selectPoints, editPoints, interactive, strokeObj, style, group, inherit)

Blob
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Makes a blob shape inside a container using a number of points.
The points have Bezier controls - little handles that change the shape of the Blob.
The type of control can be specified overall and individually - and can be hidden or shown
The type of control can be changed by double clicking the point - colors of the handles will change
Points can be added by clicking on the shape or removed by SHIFT clicking a point.
CTRL Z will undo adding or removing a point
The shape of the Blob can be recorded with the recordData() method and recreated with the setData() method
The Blob is set by default to show and hide controls when clicked
It is also draggable by default when the controls are showing

MULTIPLE SELECT
Multiple points can be selected and dragged or moved with the keyboard arrows (moves 10 pixels with shift key down)

NOTE: mouseChildren is turned to false for all zim Shape containers.
NOTE: with the ZIM namespace zns = false, this overwrites a JS Blob - so the JS Blob is stored as document.Blob

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var blob = new Blob(); // makes a circle with default 4 points with Bezier controls
blob.center(stage);

var moreBlob = new Blob({
	points:12, // 12 points for more complex shape
}).center(stage);

var specifiedBlob = new Blob({
	color:purple,
	controlType:"free", // free will be default control type (rather than "straight")
	points:[
		// the control position x, y
		// then three point positions inside the control - so relative to the control position
		// 1. circle position x, y (usually the same as the control position - so 0,0)
		// 2. the location of the first control rectangle x and y
		// 3. the location of the second control rectangle x and y
		// then an optional specific type of control that overrides the controlType parameter (or the default type of "straight")
		[-100,-100,-100,100,100,-100,0,0,"mirror"], // this will be type "mirror"
		[100,-100,100,0,-50,0], // this will be type "free" because controlType parameter
		[100,100], // these will be type "none" because no dimensions (or dimensions 0) specified for controls
		[-100,100]
	]
}).centerReg(stage).drag();
END EXAMPLE

EXAMPLE
// Transform the original points of a Blob
// If you rotate or scale, this affects the control points - the little rectangles rotate or they scale
// To avoid this, the points themselves can be transformed (scaleX, scaleY, scale, rotation, x, y)
// This makes a square and scales it bigger without affecting control size or stroke size (if there were a stroke)
// Note the default number of points is 4 but they are arranged at the top, bottom and sides - so would make a diamond with just controlType:"none"
new Blob({controlType:"none"}).transformPoints("rotation", 45).transformPoints("scale", 2).center();
END EXAMPLE

EXAMPLE
// make a Blob the shape of basic ZIM shapes
// this overrides the path parameter
new Blob({shape:"circle"}).pos(200,200);
new Blob({shape:new Rectangle(100,200)}).center();
new Blob({shape:new Triangle()}).transformPoints("rotation", 90).pos(50,50,true,true);
END EXAMPLE

EXAMPLE
// Animate along a Blob
// see https://zimjs.com/explore/blobAnimate.html for more
// see https://zimjs.com/explore/blobAnimate2.html for more
var path = new Blob().center();
new Circle(10, red).addTo().animate({path:path}, 1000);
END EXAMPLE

EXAMPLE
// Animate one Blob into another
var targetBlob = new Blob({points:"rectangle"});
var blob = new Blob({radius:50, points:"circle", interactive:false})
	.pos(200,200)
	.transformPoints("rotation", -45) // to better tween to rectangle
	.animate({
		props:{shape:targetBlob},
		time:1000,
		rewind:true,
		loop:true
	});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
color - (default green) the fill color as any CSS color including "rgba()" for alpha fill (set a to 0 for tranparent fill)
borderColor - (default null) the stroke color
borderWidth - (default 1 if stroke is set) the size of the stroke in pixels
num - get the number of points - to set, use the points property
points - (default 4) a number of points to start with to make the shape
	OR a shape string of "circle", "rectangle" or "triangle"
	OR a ZIM Circle, Rectangle or Triangle with any dimensions that will be matched
	OR an SVG path like: points:"M0,129.5c22,0,40-31,40-41c0-8-3.2-13-10-13" etc. (also see SVGContainer)
	OR an array of points as follows:
	[[controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType], [etc]]
	controlX and controlY - the x and y location of the control Container which holds the point circle and the two control rectangles
	rect1X, rect1Y, rect2X, rect2Y - (default based on controlLength) the x and y location of the control rectangles relative to the control location
	circleX and circleY - (default 0) the x and y location of the circle relative to the control location (usually 0, 0)
	controlType - (default main controlType parameter or "straight" if not controlType parameter) the point's controlType "none", "mirror", "straight" or "free"
radius - (default 100) the default radius of the circle used to create the blob (also specifies the blob's bounds(-radius, -radius, radius*2, radius*2))
controlLength - |ZIM VEE| (default radius*numPoints/4) specify a Number to override the calculated default
controlType - (default "straight") one of four String values as follows:
	none - there are no control rectangles (they are actually set at 0,0).  This makes a corner at the circle point.
	mirror - the control rectangles reflect one another about the point circle - lengths are kept even
	straight - the control rectangles keep a straight line through the point circle but length is independent
	free - the control rectangle moves independently from the other control rectangle
	** The controlType can be specified for each point - see the points parameter
	** The controlType can be changed by doubleClicking the point circle to cycle through the controls in the order above - unless the lockControlType is set to true
lockControlType - (default false) set to true to disable doubleClicking of point circles to change controlType
showControls - (default true) set to false to start with controls not showing - can change this after with control property or showControls() method
lockControls - (default false) set to true to lock the editing of controls - can't move the points or handles - but can see them if showControls is set to true
handleSize - (default 20 mobile 10 for non-mobile) the size of control boxes and affects the circles too proportionally
	If a handleSize of 0 is chosen, then the sticks will disappear too
allowToggle - (default true) set false to let turn off clicks showing and hiding controls
move - (default true) set to false to disable dragging when controls are showing
	can also set to "always" to allow movement when controls are not showing
dashed - (default false) set to true for dashed border (if borderWidth or borderColor set)
onTop - (default true) set to false to not bring shape to top of container when dragging
stickColor - (default "#111") set the stick color of the controls
selectColor - (default white) the color of the selected circle or rectangle of the controls if selectPoints is true
selectPoints - (default true) set to false to not allow point controls to be selected for keyboard control
editPoints - (default true) lets user add points by pressing on shape path.
	set to "anywhere" to let users add points anywhere - will add points with controlType:"none"
	set to false to not allow adding or removing points with click or shift click
interactive - (default true) set to false to turn off controls, move, toggle, select, edit - leaving just the shape
strokeObj - (default {caps:"butt", joints:"miter", miterLimit:10, ignoreScale:false}) set to adjust stroke properties
	caps options: "butt", "round", "square" or 0,1,2
	joints options: "miter", "round", "bevel" or 0,1,2
	miterLimit is the ration at which the mitered joint will be clipped
	ignoreScale set to true will draw the specified line thickness regardless of object scale
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
addPoint(percent, controlType) - add a point at a percent (100) of the total curve
	this is handy to make path have the same number of points for animate() path tweens
	controlType can be as specified in main points parameter
	returns object for chaining
addPoints(num, controlType, startPoint) - add num points between existing points
	controlType can be as specified in main points parameter
	specify a startPoint to add points between the startPoint and the next point (one segment of points)
	spread (default false) set to true to spread points evenly around path rather than evenly between segments
	dataOnly and points are used internally
	returns object for chaining
interpolate(num, startPoint, spread) - get point data {x,y} for this existing points and num (default 1) points inbetween
	used with hitTestPath
	specify a startPoint to get points between the startPoint and the next point (one segment of points)
	spread (default false) set to true to spread points evenly around path rather than evenly between segments
	returns an array of point objects with x, y properties and an r property for ratio of distance along path
recordData(toJSON) - returns an object with x, y, points, color, borderColor, borderWidth, move, toggle, controls PROPERTIES to be used with setData() method
	if toJSON (default false) is set to true, the return value is a JSON string
	the points data comes from the points property
setData(data, fromJSON) - sets the properties to match the data object passed in - this should come from recordData()
	if fromJSON (default false) is set to true, it will assume a JSON string is passed in as data
	the points data is parsed with the set setPoints() so the number of points should be the same
	returns object for chaining
recordPoints(popup) - returns an array with the same format as the points parameter - or can just use points property
	popup - (default false) set to true to open a zim Pane (blob.pane) with the points in a zim TextArea (blob.textArea) (click off to close)
	NOTE: the TextArea output uses JSON.stringify() - to add the points to the points parameter of the Blob use JSON.parse(output);
	NOTE: using zog(JSON.stringify(blob.recordData()))... the console will remove the quotes from the controlTypes so those would have to be manually put back in before parse() will work
setPoints(data) - sets the Blob points to the data from recordPoints
	This does not remake the Blob but rather shifts the controls so the number of points should be the same
changeControl(index, type, rect1X, rect1Y, rect2X, rect2Y, circleX, circleY, update) - change a control type and properties at an index
	accepts ZIM DUO normal parameters or configuration object literal with parameter names as propterties
	passing in null as the index will change all points to the specified properties
	the update parameter defaults to false so set to true to show update or call update() below
	this is so multiple changes can be batched before calling update - for instance when animating blobs.
transformPoints(transformType, amount, x, y) - scale, rotate, move points without affecting controls or borderWidth - returns object for chaining
	Note - does not adjust original Bounds
	transformType - String any of: "scale", "scaleX", "scaleY", "rotation", "x", "y"
	amount - the amount to transform
	x, y - (default 0, 0) the x and y position to transform about
update(normalize) - update the Blob if animating control points, etc. would do this in a Ticker
	set normalize (default false) to true to use pointsAdjusted for rotated and scaled points
	use true for manually editing points after setting rotation or scale on point
	just leave out if only animating points
showControls() - shows the controls (and returns blob) - or use  blob.controlsVisible = true property
hideControls() - hides the controls (and returns blob) - or use blob.controlsVisible = false property
toggle(state - default null) - shows controls if hidden and hides controls if showing (returns the object for chaining)
	or pass in true to show controls or false to hide controls
traverse(obj, start, end, time) - animates obj from start point to end point along path - thanks KV for the thought!
	set start point greater than end point to traverse backwards
	will dispatch a "traversed" event when done
setColorRange(color1, color2) - set a color range for shape - used by colorRange property - returns obj for chaining
	if one color is used, the current color is used and color1 is the second color in the range
getPointAngle(index) - gets the angle made by the tangent at the index provided
getSegmentPoint(point1, point2) - returns an array of [point1, controlPoint1, controlPoint2, point2]
	used internally for animating to path and adding removing Bezier points
getAdjacentSegmentData(index) - returns an array of two arrays:
	The first is an array of cubic Bezier points for segments adjacent and including the provided point index
	each element is in the form of [point1, controlPoint1, controlPoint2, point2]
	The second is an array of starting point indexes for the segments that were tested
	used internally to drag an animation along the path
	will wrap around the blob if needed
getCurvePoint(ratio, segmentRatios, segmentPoints) gets a point along whole curve at the ratio (0-1) provided
	along with x and y values, the point has a z value that is the index of the blob point before the calculated point
	the point also has an angle property which is the angle of the tangent at the point
	ratio is 0-1 with 0 being at the first point and 1 being at the end of the last segment (the first point)
	segmentRatios and segmentPoints will be calculated if not provided
	used internally for animating along the path - if lockControls is true, only animate will precalculate these values
linearGradient([colors],[ratios], x0,y0, x1,y1) - shortcut to colorCommand linearGradient method (see properties below)
radialGradient([colors],[ratios], x0,y0,radius0, x1,y1,radius1) - shortcut to colorCommand radialGradient method (see properties below)
cache(see Container docs for parameter description) - overrides CreateJS cache() and returns object for chaining
	Leave parameters blank to cache bounds of shape (plus outer edge of border if borderWidth > 0)
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy of the shape
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
shape - gives access to the shape of the blob
color - get and set the fill color
colorRange - if setColorRange() is used, the colorRange is a ratio (0-1) between the colors
	setting the colorRange will change the color property of the shape
	for instance, shape.setColorRange(blue, pink) then shape.colorRange = .5
	will set the color of the shape to half way between blue and pink
	shape.animate({color:red}, 1000); is a shortcut to animate the colorRange
	shape.wiggle("colorRange", .5, .2, .5, 1000, 5000) will wiggle the colorRange
colorCommand - access to the CreateJS fill command for bitmap, linearGradient and radialGradient fills
	eg. shape.colorCommand.linearGradient([green, blue ,green], [.2, .5, .8], 0, 0, shape.width, 0)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Fill.html
borderColor - get and set the stroke color
borderColorCommand - access to the CreateJS stroke command for bitmap, linearGradient and radialGradient strokes
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.Stroke.html
borderWidth - get and set the stroke size in pixels
borderWidthCommand - access to the CreateJS stroke style command (width, caps, joints, miter, ignoreScale)
	See: https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeStyle.html
borderDashedCommand - access to the CreateJS stroke dashed command (segments, offset)
	see https://www.createjs.com/docs/easeljs/classes/Graphics.StrokeDash.html
stickColor - get or set the stick color of the controls - requires an update() to see changes
points - get or set the points array of the Blob in the same format as the points parameter:
	a number, a shape string ("circle", "rectangle", "triangle"), a ZIM Circle, Rectangle, Triangle
	or an array as follows:
	[[controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType], [etc]]
pointsAdjusted - get points with any point rotation converted to 0 - see update(true)
pointControls - get an array of controls (a container) - use this to animate controls
pointCircles - get an array of control circles - use this to place some other object at the point
pointObjects - get an array of point objects for each point in the following format:
	[[control, circle, rect1, rect2, controlType], [etc.]]
	control - the container for the control that holds the circle and rectangles (also see pointControls)
	circle - the control point circle (also see pointCircles)
	rect1 - the first control point rectangle
	rect2 - the second control point rectangle
	controlType - the control type: default is "straight" (or null) and there is also "mirror", "free" and "none"
	NOTE: control, circle, rect1, rect2 can be positioned or animated and controlType can be changed
	NOTE: the update() method must be called if manually changing the control positions or type
	NOTE: if constantly animating the controls then use a Ticker.add(function(){blob.update();})
	NOTE: also see recordData(), setData(), recordPoints(), setPoints() methods for further options
addPointFactor - (default 20) used when placing new points along edge (editPoints is true)
	divides the distance between points by this amount - the smaller the more accurate but also slower
addMinDistance - (default 15) edge press needs to be within this distance to add a point to the edge
segmentPoints - a read-only array of cubic Bezier points for each segment
	each element is in the form of [point1, controlPoint1, controlPoint2, point2]
	used internally to animate to the path and add and remove Bezier points
segmentRatios - a read-only array of cumulative ratio lengths of segments
	for instance the default four points is [.25, .5, .75, 1]
	used internally to animate to the path and attribute proportional time to each segment
controls - access to the container that holds the sets of controls
	each control is given a read-only num property
sticks - access to the container that holds the control sticks
lastSelected - access to the last selected (or created) control container
lastSelectedIndex - the index number of the last selected controls
controlsVisible - get or set the visibility of the controls - or use showControls() and hideControls()
types - get or set the general array for the types ["mirror", "straight", "free", "none"]
	changing this or removing a type will adjust the order when the user double clicks the points to change their type
	this is not an array of types for each point - see the points property to access the types of each point
lockControls - Boolean to lock controls from being adjusted or not
allowToggle - Boolean to get or set clicking to show and hide controls
move - Boolean to drag or not drag Blob when controls are showing
	can also set to "always" to allow movement when controls are not showing
lockControlType - Boolean to lock the type of the controls in their current state or not
onTop - get or set the onTop property
selectPoints - get or set whether points can be selected
interactive - get or set whether the shape is interactive - toggle, move, change or add controls, etc.
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
keyFocus - get or set the keyboard focus on the DisplayObject - see also zim.KEYFOCUS
   will be set to true if this DisplayObject is the first made or DisplayObject is the last to be used with keyboard
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "change" event for when the bezier controls are adjusted (pressup only)
	if monitoring constant change is needed add a pressmove event to Blob.sets
	the change event object has a transformType property with values of "move", "bezierPoint", "bezierHandle", "bezierSwitch"
dispatches "controlsshow" and "controlshide" events when clicked off and on and toggle is true
dispatches an "update" event if the points are changed or a point is added or removed
	this removes all listeners on the old shape and controls
	so any custom listeners on shape and controls will need to be re-applied - use the update event to do so
dispatches a "traversed" event when traverse() is done - the event object has an obj property for the traversing object
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+53.5
	zim.Blob = function(color, borderColor, borderWidth, points, radius, controlLength, controlType, lockControlType, showControls, lockControls, handleSize, allowToggle, move, dashed, onTop, stickColor, selectColor, selectPoints, editPoints, interactive, strokeObj, style, group, inherit) {
		var sig = "color, borderColor, borderWidth, points, radius, controlLength, controlType, lockControlType, showControls, lockControls, handleSize, allowToggle, move, dashed, onTop, stickColor, selectColor, selectPoints, editPoints, interactive, strokeObj, style, group, inherit";
		var duo; if (duo = zob(zim.Blob, arguments, sig, this)) return duo;
		z_d("53.5");
		this.group = group;
		var DS = style===false?{}:zim.getStyle("Blob", this.group, inherit);

		if (zot(radius)) radius = DS.radius!=null?DS.radius:100;
		this.zimContainer_constructor(-radius,-radius,radius*2,radius*2,false);
		this.type = "Blob";

		if (zot(dashed)) dashed = DS.dashed!=null?DS.dashed:false;
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:null;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(color)) color = DS.color!=null?DS.color:(borderWidth>0?"rgba(0,0,0,0)":zim.green);
		if (color.style) {this.colorCommand = color; color = "black";}
		if (borderColor && borderColor.style) {this.borderColorCommand = borderColor; borderColor = "black";}
		if (zot(points)) points = DS.points!=null?DS.points:4;
		var num = typeof points == "number" ? points : points.length;
		var controlLengthOriginal = controlLength;
		if (zot(controlLength)) controlLength = DS.controlLength!=null?DS.controlLength:(radius * 4 / num);
		if (zot(controlType)) controlType = DS.controlType!=null?DS.controlType:null;
		var originalControlType = controlType;
		if (zot(controlType)) controlType = "straight";
		if (zot(lockControlType)) lockControlType = DS.lockControlType!=null?DS.lockControlType:false;
		if (zot(interactive)) interactive = DS.interactive!=null?DS.interactive:true;
		if (zot(showControls)) showControls = DS.showControls!=null?DS.showControls:interactive;
		var _controls = showControls;
		if (zot(lockControls)) lockControls = DS.lockControls!=null?DS.lockControls:!interactive;
		if (zot(handleSize)) handleSize = DS.handleSize!=null?DS.handleSize:(zim.mobile()?20:10);
		if (zot(allowToggle)) allowToggle = DS.allowToggle!=null?DS.allowToggle:interactive;
		if (zot(move)) move = DS.move!=null?DS.move:interactive;
		if (zot(stickColor)) stickColor = DS.stickColor!=null?DS.stickColor:"#111";
		if (zot(selectColor)) selectColor = DS.selectColor!=null?DS.selectColor:"#fff";
		if (zot(selectPoints)) selectPoints = DS.selectPoints!=null?DS.selectPoints:interactive;
		this.stickColor = stickColor;

		if (zot(onTop)) onTop = DS.onTop!=null?DS.onTop:true;
		if (zot(editPoints)) editPoints = DS.editPoints!=null?DS.editPoints:interactive;
		if (zot(strokeObj)) strokeObj = DS.strokeObj!=null?DS.strokeObj:{};

		var that = this;
		this.interactive = interactive;
		this.num = num;
		this.editPoints = editPoints;
		this.selectPoints = selectPoints;
		this.lockControls = lockControls;
		this.onTop = onTop;
		this.move = move;
		this.allowToggle = allowToggle;
		this.lockControlType = lockControlType;

		var types = this.types = ["mirror", "straight", "free", "none"];

		var _points;
		var _pointCircles;
		var _pointControls;
		var _color = color;
		var _borderColor = borderColor;
		var _borderWidth = borderWidth;
		var colorObj;
		var borderColorObj;
		var borderWidthObj;
		var borderDashedObj;

		var shape;
		var moveDownEvent;
		var movePressEvent;
		var moveUpEvent;
		var stage;

		var draggingCheck = that.move;
		var min = 2; // distance within which to count as click to add point

		var mapMove;
		var drawShape;
		var sets;

		points = checkForShape(points);
		function checkForShape(shape) {
			if (shape=="circle" || shape == "rectangle" || shape =="triangle") {
				if (shape == "circle") shape = new Circle(radius);
				if (shape == "rectangle") shape = new Rectangle(radius*2,radius*2).centerReg({add:false});
				if (shape == "triangle") shape = new Triangle(radius*2,radius*2,radius*2);
			}
			var points;
			if (shape.type == "Circle" || shape.type == "Rectangle" || shape.type == "Triangle") {
				var b = shape.getBounds();
				if (shape.type == "Circle") {
					points = 4;
					controlLengthOriginal = controlLength = radius*2*.5523;
				} else if (shape.type == "Rectangle") {
					shape.centerReg({add:false});
					points = [[b.x-b.width/2, b.y-b.height/2],[b.x+b.width/2, b.y-b.height/2],[b.x+b.width/2, b.y+b.height/2],[b.x-b.width/2, b.y+b.height/2]];
					that.setBounds(b.x-b.width/2, b.y-b.height/2, b.width, b.height);
					that.regX = shape.regX-b.width/2;
					that.regY = shape.regY-b.height/2;
					// this.rotation = shape.rotation;
					// this.scaleX = shape.scaleX;
					// this.scaleY = shape.scaleY;
				} else if (shape.type == "Triangle") {
					points = [[shape.three.x-shape.width/2, shape.three.y+shape.height/2+shape.adjusted],[shape.two.x-shape.width/2, shape.two.y+shape.height/2+shape.adjusted],[shape.one.x-shape.width/2, shape.one.y+shape.height/2+shape.adjusted]];
					that.setBounds(b.x-b.width/2, b.y-b.height/2, b.width, b.height);
				}
				return points;
			} else {
				return shape;
			}
		}
		if (typeof points == "string") {
			var svgProcessor = new zim.SVGContainer();
			points = svgProcessor.processPath(points);
		}

		if (originalControlType && typeof points != "number") {
			// override controlType
			loop(points, function(point) {
				point[8]=originalControlType;
				if (originalControlType == "none") {
					point[4]=point[5]=point[6]=point[7]=0;
				}
			});
		}

		init();
		function init() {
			if (sets) sets.removeAllEventListeners();

			if (that.selectPoints) {
				that.selectedBalls = new zim.SelectionSet();
				that.selectedRect1s = new zim.SelectionSet();
				that.selectedRect2s = new zim.SelectionSet();

				that.selectionManager = new zim.SelectionManager([
					that.selectedBalls,
					that.selectedRect1s,
					that.selectedRect2s
				], "ctrl", false);
			} else {
				that.selectionManager = new zim.SelectionManager(null, "ctrl");
			}

			num = typeof points == "number" ? points : points.length;
			if (num <= 0) return;
			if (zot(controlLengthOriginal)) controlLength = radius * 4 / num;

			shape = that.shape = new zim.Shape({style:false}).addTo(that);
			var sticks = that.sticks = new zim.Shape({style:false}).addTo(that);
			if (handleSize <= 0) sticks.removeFrom();
			var g = shape.graphics;
			g.c();
			var s = sticks.graphics;
			s.c();

			var ballS = handleSize/10*8;
			var rectS = handleSize;

			var mobile = zim.mobile();

			sets = that.controls = new zim.Container({style:false}).addTo(that); // sets - a set contains a ball and two rects
			if (that.interactive) sets.drag({onTop:!mobile});

			_points = [];
			_pointControls = [];
			_pointCircles = [];

			var angle, point, temp, set, rect1, rect2, ball, type, setInfo;

			for (var i=0; i<num; i++) {
				set = new zim.Container({style:false}).addTo(sets);
				set.num = i;
				if (typeof points == "number") { // no sets yet

					// easier to create controls in a temp vertical Container
					// set the registration point at the circle center
					// then rotate the temp container
					// then get the resulting rotated coordinates and use localToLocal
					// to find coordinates of controls in set Container
					// afterwards, adjust controls in set Container so origin and registration is at ball
					// then move the set Container so it matches that adjustment
					// (or could have calculated all positions to start with aTan2, sin, cos etc.)
					var length = zim.Pick.choose(controlLength);
					temp = new zim.Container(length, radius, null, null, false).reg(length/2, radius).addTo(that);
					temp.rotation = i/num * 360;
					ball = new zim.Circle(ballS, that.selectPoints&&that.selectedBalls.isSelected(i)?selectColor:zim.light, zim.dark, 2, null, null, null, false)
						.centerReg(temp)
						.loc({x:length/2,y:0});
					rect1 = new zim.Rectangle(rectS, rectS, that.selectPoints&&that.selectedRect1s.isSelected(i)?selectColor:getBackgroundColor(controlType), handleSize==0?null:zim.dark, handleSize==0?null:2, null, null, null, false)
						.centerReg(temp)
						.loc({x:0,y:0});
					rect2 = new zim.Rectangle(rectS, rectS, that.selectPoints&&that.selectedRect2s.isSelected(i)?selectColor:getBackgroundColor(controlType), handleSize==0?null:zim.dark, handleSize==0?null:2, null, null, null, false)
						.centerReg(temp)
						.loc({x:length,y:0});

					var ballPoint = temp.localToLocal(ball.x, ball.y, sets);
					ball.x = ballPoint.x;
					ball.y = ballPoint.y;
					ball.addTo(set, null, false);
					var rect1Point = temp.localToLocal(rect1.x, rect1.y, sets);
					rect1.x = controlType=="none"?0:rect1Point.x-ball.x;
					rect1.y = controlType=="none"?0:rect1Point.y-ball.y;
					rect1.addTo(set, null, false);
					var rect2Point = temp.localToLocal(rect2.x, rect2.y, sets);
					rect2.x = controlType=="none"?0:rect2Point.x-ball.x;
					rect2.y = controlType=="none"?0:rect2Point.y-ball.y;
					rect2.addTo(set, null, false);
					set.x = ball.x;
					set.y = ball.y;
					ball.x = 0;
					ball.y = 0;
					if (controlType=="none") ball.addTo(set, null, false); // on top

				} else { // passing in set data

					// balls are relative to blob but handles are relative to ball
					// points is an array of [[setX, setY, ballX, ballY, handleX, handleY, handle2X, handle2Y, type], etc.]

					setInfo = points[i];
					type = setInfo[8] ? setInfo[8] : controlType;
					set.loc({x:setInfo[0], y:setInfo[1]});
					ball = new zim.Circle(ballS, zim.light, zim.dark, 2, null, null, null, false)
						.centerReg(set)
						.loc({x:setInfo[2],y:setInfo[3]});
					rect1 = new zim.Rectangle(rectS, rectS, getBackgroundColor(type), handleSize==0?null:zim.dark, handleSize==0?null:2, null, null, null, false)
						.centerReg(set, 0)
						.loc({x:setInfo[4],y:setInfo[5]});
					rect2 = new zim.Rectangle(rectS, rectS, getBackgroundColor(type), handleSize==0?null:zim.dark, handleSize==0?null:2, null, null, null, false)
						.centerReg(set, 0)
						.loc({x:setInfo[6],y:setInfo[7]});
				}

				ball.mySet = set;
				ball.rect1 = rect1;
				ball.rect2 = rect2;
				ball.index = i;

				if (handleSize==0) {
					ball.expand(10);
					rect1.expand(10);
					rect2.expand(10);
				}

				if (mobile) {
					ball.on("mousedown", mobileDouble);
				} else {
					ball.on("dblclick", doubleIt);
				}

				rect1.ball = ball;
				rect1.other = rect2;
				rect2.ball = ball;
				rect2.other = rect1;

				if (mobile) {
					ball.expand();
					rect1.expand();
					rect2.expand();
				}

				point = [set, ball, rect1, rect2, setInfo?setInfo[8]:controlType];
				_points.push(point);
				_pointCircles.push(ball);
				_pointControls.push(set);
			}

			var tappedTwice = false;
			function mobileDouble(e) {
				if (!tappedTwice) {
					tappedTwice = true;
					setTimeout(function() {
						tappedTwice = false;
					}, 300);
				} else {
					e.preventDefault();
					doubleIt(e);
				}
			}

			function doubleIt(e) {
				if (that.lockControlType) return;
				var ball = e.target;
				// cycle through the types
				var type = _points[ball.index][4] ? _points[ball.index][4] : controlType;
				if (Math.abs(ball.rect1.x) <= 2 && Math.abs(ball.rect1.y) <= 2 && Math.abs(ball.rect2.x) <= 2 && Math.abs(ball.rect2.y) <= 2) {
					type = "none"
				}
				if (type == "none") {
					ball.parent.addChildAt(ball, 0);
				}
				// modulus going backwards needs to add the length so it does not go negative
				type = that.types[(that.types.indexOf(type)+(that.shiftKey?-1:1)+that.types.length)%that.types.length];
				if (type == "none") {
					ball.rect1.x =  ball.rect1.y =  ball.rect2.x =  ball.rect2.y = 0;
					ball.parent.addChild(ball);
					e.stopImmediatePropagation();
				}
				_points[ball.index][4] = type;
				ball.rect1.color = getBackgroundColor(type);
				ball.rect2.color = getBackgroundColor(type);
				drawShape();
				var ev = new createjs.Event("change");
				ev.controlType = "bezierSwitch";
				that.dispatchEvent(ev);
				ball.stage.update();
			};

			function getBackgroundColor(type) {
				var colors = {mirror:zim.purple, free:zim.yellow, none:zim.blue};
				return colors[type] ? colors[type] : zim.pink;
			}

			drawShape = function() {
				g.c();
				if (!that.colorCommand) that.colorCommand = colorObj = g.f(_color).command;
				// border of 0 or a string value still draws a border in CreateJS
				if (zot(_borderWidth) || _borderWidth > 0) { // no border specified or a border > 0
					if (!zot(_borderColor) || !zot(_borderWidth)) { // either a border color or thickness
						if (zot(_borderColor)) _borderColor = "black";
						if (!that.borderColorCommand) that.borderColorCommand = borderColorObj = g.s(_borderColor).command;
						if (!that.borderWidthCommand) that.borderWidthCommand = borderWidthObj = g.ss(_borderWidth, strokeObj.caps, strokeObj.joints, strokeObj.miterLimit, strokeObj.ignoreScale).command;
						if (dashed) {
							if (!that.borderDashedCommand) that.borderDashedCommand = borderDashedObj = g.sd([10, 10], 5).command;
						}
					}
				}
				var set = _points[0][0];
				var ballPoint = set.localToLocal(_points[0][1].x, _points[0][1].y, shape);
				g.mt(ballPoint.x, ballPoint.y);

				s.c().s(that.stickColor).ss(1);

				var currentIndex; var nextIndex;
				for (var i=0; i<_points.length; i++) {
					var currentIndex = i;
					var nextIndex = (i+1)%_points.length;

					var set = _points[currentIndex][0];
					var ball = _points[currentIndex][1];
					var control1 = _points[currentIndex][2];
					var control2 = _points[currentIndex][3];

					var nextSet = _points[nextIndex][0];
					var nextBall = _points[nextIndex][1];
					var nextControl1 = _points[nextIndex][2];
					var nextControl2 = _points[nextIndex][3];

					var control2Point = set.localToLocal(control2.x, control2.y, shape);
					var nextControl1Point = nextSet.localToLocal(nextControl1.x, nextControl1.y, shape);
					var nextBallPoint = nextSet.localToLocal(nextBall.x, nextBall.y, shape);

					g.bt(
						control2Point.x, control2Point.y,
						nextControl1Point.x, nextControl1Point.y,
						nextBallPoint.x, nextBallPoint.y
					);


					// create the sticks
					var ballPoint = set.localToLocal(ball.x, ball.y, shape);
					var control1Point = set.localToLocal(control1.x, control1.y, shape);

					s.mt(ballPoint.x, ballPoint.y).lt(control1Point.x, control1Point.y);
					s.mt(ballPoint.x, ballPoint.y).lt(control2Point.x, control2Point.y);
				}
				g.cp();

				g.append(that.colorCommand);
				if (dashed) g.append(that.borderDashedCommand);
				if (that.borderWidthCommand) g.append(that.borderWidthCommand);
				if (that.borderColorCommand) g.append(that.borderColorCommand);
			}
			drawShape();

			var startPosition;
			sets.on("mousedown", function(e) {
				if (that.lockControls) return;
				if (that.selectPoints) that.keyFocus = true;
				startPosition = {x:e.target.x, y:e.target.y};
				if (e.target.rect1) { // then mousedown on ball
					var ball = e.target;
					ball.startX = ball.x;
					ball.startY = ball.y;
					ball.rect1.startX = ball.rect1.x;
					ball.rect1.startY = ball.rect1.y;
					ball.rect2.startX = ball.rect2.x;
					ball.rect2.startY = ball.rect2.y;
				} else { // mousedown on control
					var rect = e.target;
					rect.startX = rect.x;
					rect.startY = rect.y;
					var ball = rect.ball;
					var index = ball.index;
					var type = controlType;
					if (!zot(_points[index][4])) type = _points[index][4];
					if (type == "straight") {
						var other = rect.other;
						var dX = other.x - ball.x;
						var dY = other.y - ball.y;
						other.stickLength = Math.sqrt(Math.pow(dX,2) + Math.pow(dY,2));
					}
				}
				if (that.selectPoints) {
					// need to reset all start points for each control circle and rectangle moved
					var currentSet = that.selectionManager.currentSet;
					if (currentSet && currentSet.selections && currentSet.selections.length > 0) {
						for(var i=0; i<currentSet.selections.length; i++) {
							var point = that.pointObjects[currentSet.selections[i]];
							point[1].startX = point[1].x;
							point[1].startY = point[1].y;
							point[2].startX = point[2].x;
							point[2].startY = point[2].y;
							point[3].startX = point[3].x;
							point[3].startY = point[3].y;
						}
					}
				}
			});

			if (that.selectPoints) {
				sets.tap(function (e) {
					if (e.target.rect1) { // then mousedown on ball - which has a rect1
						var ball = e.target;
						that.selectedBalls.toggle(ball.parent.num);
					} else { // mousedown on control
						var rect = e.target;
						rect.color = "white";
						var ball = rect.ball;
						if (ball.rect1 == rect) that.selectedRect1s.toggle(ball.parent.num);
						else that.selectedRect2s.toggle(ball.parent.num);
					}
					// loop through all controls and set to right color based on selection
					for (var i=0; i<that.pointObjects.length; i++) {
						var po = that.pointObjects[i];
						po[1].color = that.selectedBalls.isSelected(i)?zim.white:zim.light;
						po[2].color = that.selectedRect1s.isSelected(i)?zim.white:getBackgroundColor(po[4]);
						po[3].color = that.selectedRect2s.isSelected(i)?zim.white:getBackgroundColor(po[4]);
					}
					e.target.stage.update();
				});
			}

			sets.on("pressmove", function(e) {
				if (that.lockControls) return;
				if (that.selectPoints) {
					var currentSelected = getCurrentSelected();
					if (currentSelected.indexOf(e.target) == -1) {
						mapMove(e.target);
						drawShape();
					} else {
						if (currentSelected.length > 0) {
							diffX = e.target.x-e.target.startX;
							diffY = e.target.y-e.target.startY;
							for(var i=0; i<currentSelected.length; i++) {
								var pointObj = currentSelected[i];
								pointObj.x = pointObj.startX + diffX;
								pointObj.y = pointObj.startY + diffY;
								mapMove(pointObj);
							}
							drawShape();
						}
					}
				} else {
					mapMove(e.target);
					drawShape();
				}
			});


			sets.on("pressup", function(e) {
				if (that.lockControls) return;
				var moveControlCheck = (e.target.x != startPosition.x || e.target.y != startPosition.y);
				var ev = new createjs.Event("change");
				if (e.target.rect1) { // pressup on ball
					ev.controlType = "bezierPoint";
					endMove(e.target);
				} else {
					ev.controlType = "bezierHandle";
				}
				if (moveControlCheck) that.dispatchEvent(ev);
			});

			function endMove(target) {
				if (that.selectPoints) {
					var currentSelected = getCurrentSelected();
					if (currentSelected && currentSelected.indexOf(target) == -1) {
						replaceControls(target);
					} else if (currentSelected && currentSelected.length>0) {
						for(var i=0; i<currentSelected.length; i++) {
							replaceControls(currentSelected[i]);
						}
					} else {
						replaceControls(target);
					}
				} else {
					replaceControls(target);
				}
			};

			that.changeControl = function(index, type, rect1X, rect1Y, rect2X, rect2Y, circleX, circleY, update) {
				var sig = "index, type, rect1X, rect1Y, rect2X, rect2Y, circleX, circleY, update";
				var duo; if (duo = zob(that.changeControl, arguments, sig)) return duo;
				if (zot(index)) {
					for (var i=0; i<_points.length; i++) {
						that.changeControl(i, type, rect1X, rect1Y, rect2X, rect2Y, circleX, circleY);
					}
					return that;
				}
				var point = _points[index];
				point[4] = type;
				if (type == "none") {
					if (!zot(circleX)) point[1].x = circleX;
					if (!zot(circleY)) point[1].y = circleY;
					point[2].x = point[1].x,
					point[2].y = point[1].y;
					point[3].x = point[1].x,
					point[3].y = point[1].y;
					point[1].parent.addChild(point[1]);
				} else {
					if (!zot(circleX)) point[1].x = circleX;
					if (!zot(circleY)) point[1].y = circleY;
					if (!zot(rect1X)) point[2].x = rect1X;
					if (!zot(rect1Y)) point[2].y = rect1Y;
					if (!zot(rect2X)) point[3].x = rect2X;
					if (!zot(rect2Y)) point[3].y = rect2Y;
					point[1].parent.addChildAt(point[1], 0);
				}
				point[2].color = getBackgroundColor(type);
				point[3].color = getBackgroundColor(type);
				if (update) {
					that.update();
					if (that.stage) that.stage.update();
				}
				return that;
			}

			that.transformPoints = function(transformType, amount, x, y) {
				that.points = zim.transformPoints(that.points, transformType, amount, x, y);
				return that;
			}

			that.traverse = function(obj, start, end, time) {
				var ratios = zim.copy(that.segmentRatios);
				ratios.unshift(0);
				if (zot(end)) end = start+1;
				var forward = start < end;
				if (forward) {
					var startPercent = ratios[start]*100;
					var endPercent = ratios[end]*100;
				} else {
					var startPercent = 50 + (100 - ratios[start]*100)/2;
					var endPercent = 50 + (100 - ratios[end]*100)/2;
				}
				obj.percentComplete = startPercent;
				obj.animate({
					ease:"linear",
					props:{path:that},
					rewind:!forward,
					time:time,
					events:true
				});
				obj.on("animation", function (e) {
					// when it hits the end it may start over
					if (obj.percentComplete > endPercent || obj.percentComplete == 0) {
						obj.stopAnimate();
						e.remove();
						var eventObj = new createjs.Event("traversed");
						eventObj.obj = obj;
						that.dispatchEvent(eventObj);
					}
				});
				return that;
			}

			that.update = function(normalize) {
				if (normalize) {
					// located any rotated or scaled points
					// and set them back to non-rotated and non-scaled
					// but keep control handles at the earlier positions
					// need to normalize before doing more manual updates with Beziers
					// do not need to normalize if animating blob points
					that.points = that.pointsAdjusted;
				} else {
					drawShape();
				}
				return that;
			}

			if (that.move && that.interactive) shape.drag({onTop:false});
			moveDownEvent = shape.on("mousedown", function() {
				startPosition = {x:shape.x, y:shape.y};
				if (that.selectPoints) that.keyFocus = true;
				upTop();
			});
			movePressEvent = shape.on("pressmove", function() {
				sets.x = shape.x;
				sets.y = shape.y;
				sticks.x = shape.x;
				sticks.y = shape.y;
			});
			moveUpEvent = shape.on("pressup", function() {
				var moveControlCheck = (shape.x != startPosition.x || shape.y != startPosition.y);
				var movePoint = shape.localToLocal(0,0,that.parent);
				that.x = movePoint.x;
				that.y = movePoint.y;
				sets.x = sets.y = sticks.x = sticks.y = shape.x = shape.y = 0;
				if (moveControlCheck) {
					var ev = new createjs.Event("change");
					ev.controlType = "move";
					that.dispatchEvent(ev);
				}
				that.stage.update();
			});

			if (!move) stopDragging(true);

			function upTop() {
				if (that.onTop) {
					var nc = that.parent.numChildren-1;
					if (that.parent.getChildAt(nc).type == "Keyboard") nc--;
					that.parent.setChildIndex(that, nc);
				}
			}

			that.toggleEvent = that.on("mousedown", function() {
				if (!that.allowToggle) return;
				if (!_controls) {
					that.showControls();
					that.dispatchEvent("controlsshow");
				}
			});

			that.added(function() {
				that.toggleStageEvent = that.stage.on("stagemousedown", function(e) {
					if (!that.allowToggle || !that.stage) return;
					if (_controls && !that.hitTestPoint(e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY, false)) {
						that.hideControls();
						that.dispatchEvent("controlshide");
					}
				});
			});

			that.clickEvent = that.on("click", function() {
				if (that.ctrlKey) {
					setTimeout(function() { // give time for record to work if drag with ctrl down
						that.clone(true).addTo(that.stage).mov(100);
						if (that.allowToggle) {
							that.hideControls();
							that.dispatchEvent("controlshide");
						}
						var ev = new createjs.Event("change");
						ev.controlType = "move";
						that.dispatchEvent(ev);
						that.stage.update();
					}, 50);
				}
			});

			that.hideControls = function() {
				that.toggled = false;
				sets.visible = false;
				sticks.visible = false;
				_controls = false;
				if (that.stage) that.stage.update();
				if (!that.allowToggle && that.move) stopDragging();
				return that;
			}
			if (!_controls) that.hideControls();
			that.showControls = function() {
				// if call this with code then will not trigger a change event - not good for TransformManager.persist()
				that.toggled = true;
				sets.visible = true;
				sticks.visible = true;
				_controls = true;
				sets.x = shape.x;
				sets.y = shape.y;
				sticks.x = shape.x;
				sticks.y = shape.y;
				that.addChildAt(shape,0); // put to bottom incase dragged
				if (that.move && !that.allowToggle) startDragging();
				if (that.stage) that.stage.update();
				return that;
			}

			that.toggle = function(state) {
				if (state===true) that.showControls();
				else if (state===false) that.hideControls();
				else if (_controls) that.hideControls();
				else that.showControls();
				return that;
			}

			that.recordData = function(toJSON) {
				if (zot(toJSON)) toJSON = false;
				var obj = {
					type:"Blob",
					index:that.parent?that.parent.getChildIndex(that):-1,
					x:that.x, y:that.y,
					points:that.recordPoints(),
					color:that.color,
					borderColor:that.borderColor,
					borderWidth:that.borderWidth,
					move:that.move,
					toggle:that.allowToggle,
					controlsVisible:_controls
				}
				if (toJSON) return JSON.stringify(obj);
				return obj;
			}

			that.setData = function(data, fromJSON) {
				if (zot(data)) return;
				if (fromJSON) {
					try{
					  data = JSON.parse(data);
					} catch(e) {
					  return;
				  	}
				}
				var index = data.index;
				if (zot(index)) index = -1;
				delete data.index;

				var pointData = data.points;
				if (!zot(pointData)) that.setPoints(pointData);
				delete data.points;
				this.num = pointData.length;

				for (var d in data) {
					that[d] = data[d];
				}
				if (that.parent) {
					that.parent.setChildIndex(that, index);
				}
				return that;
			}

			that.recordPoints = function(popup) {
				// _pointCircles are relative to blob but handles are relative to ball
				// points is an array of [[ballX, ballY, handleX, handleY, handle2X, handle2Y, type], etc.]
				if (zot(popup)) popup = false;
				var points = that.points;
				if (popup) {
					if (!pane) {
						var pane = that.pane = new zim.Pane({
							displayClose:false,
							container:that.stage,
							width:Math.min(500, that.stage.width-20),
							height:Math.min(500, that.stage.height-20),
							draggable:true,
						});
						var textArea = that.textArea = new zim.TextArea(Math.min(400, that.stage.width-70), Math.min(400, that.stage.height-70));
						textArea.centerReg(pane);
					}
					textArea.text = JSON.stringify(points);
					pane.show();
				}
				return points;
			}

			that.setPoints = function(points) {
				// adjust blob to match points passed in from recordPoints
				var p;
				var p2;
				for (var i=0; i<points.length; i++) {
					p = _points[i];
					p2 = points[i];
					if (zot(p)) continue;
					p[0].x = p2[0];
					p[0].y = p2[1];
					p[1].x = p2[2];
					p[1].y = p2[3];
					p[2].x = p2[4];
					p[2].y = p2[5];
					p[3].x = p2[6];
					p[3].y = p2[7];
					p[4] = p2[8];
					p[2].color = getBackgroundColor(p[4]);
					p[3].color = getBackgroundColor(p[4]);
				}
				that.update();
				return that;
			}
			if (style!==false) zimStyleTransforms(that, DS);
			that.clone = function(commands) {
				var color = commands?that.colorCommand:that.color;
				var color = commands?that.colorCommand:that.color;
				var newShape =  that.cloneProps(new zim.Blob(commands?that.colorCommand:that.color, commands?that.borderColorCommand:that.borderColor, that.borderWidth, that.recordPoints(), radius, controlLength, controlType, lockControlType, sets.visible, lockControls, handleSize, that.allowToggle, that.move, dashed, onTop, stickColor, selectColor, selectPoints, that.editPoints, interactive, strokeObj, style, that.group, inherit));
				if (that.linearGradientParams) newShape.linearGradient.apply(newShape, that.linearGradientParams);
				if (that.radialGradientParams) newShape.radialGradient.apply(newShape, that.linearGradientParams);
				return newShape;
			}

			// to add a control - make sure click in one spot - not drag
			that.shapeMousedownEvent = that.shape.on("mousedown", function (e) {
				stage = e.target.stage;
				if (!that.editPoints) return;
				if (that.controlsVisible) {
					that.pressX = e.stageX/stage.scaleX;
					that.pressY = e.stageY/stage.scaleY;
				} else {
					that.pressX = null;
					that.pressY = null;
				}
			});
			that.addPointFactor = 20;
			that.addMinDistance = 15;
			that.shapePressupEvent = that.shape.on("pressup", function (e) {

				if (!that.editPoints) return;
				if (that.pressX && Math.abs(that.pressX-e.stageX/stage.scaleX) < min && Math.abs(that.pressY-e.stageY/stage.scaleY) < min) {
					if (that.selectPoints) that.lastPoints = zim.copy(that.points);
					var points = that.points;
					var point = that.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
					var pointBefore = zim.closestPointAlongCurve(point, that.segmentPoints);
					if (that.editPoints == "anywhere") {
						points.splice(pointBefore+1, 0, [point.x, point.y, 0,0, 0,0, 0,0]);
						that.points = points;
						that.changeControl({index:pointBefore+1, type:"mirror", update:true});
					} else { // only on edge
						// test close enough to edge otherwise return
						var p = that.pointsAdjusted;
						var cubic = that.getSegmentPoint(p[pointBefore], p[(pointBefore+1)%p.length]);
						var length = zim.distanceAlongCurve(cubic);
						var testNum = Math.round(length/that.addPointFactor);
						var testPoints = that.interpolate(testNum, pointBefore, false, points);
						var closest=10000;
						var closestPoint;
						var closestIndex;

						zim.loop(testPoints, function (p, k) {
							if (k==0) return; // don't put on existing point
							var d = zim.dist(p, point);
							if (d < closest) {
								closest = d;
								closestIndex = k;
								closestPoint = p;
							}
						});
						if (closest < that.addMinDistance) {
							var ratios = that.segmentRatios;
							var currentRatio = ratios[pointBefore];
							var lastRatio = pointBefore>0?ratios[pointBefore-1]:0;
							that.addPoint(100*(lastRatio+(currentRatio-lastRatio)*(closestIndex/testNum)));
						}
					}
					that.lastSelectedIndex = pointBefore+1;
					that.lastSelected = that.controls.getChildAt(that.lastSelectedIndex);
					that.stage.update();
				}
			});

			// remove point
			that.controlsClickEvent = that.controls.on("click", function (e) {
				that.lastSelected = e.target.parent;
				var index = that.lastSelectedIndex = that.controls.getChildIndex(e.target.parent);
				if (!that.editPoints) return;
				if (that.selectionManager.shiftKey) { // remove
					if (e.target.type == "Circle") {
						if (that.controls.numChildren <= 2) return;
						var points = that.points;
						if (that.selectPoints) that.lastPoints = zim.copy(points);
						points.splice(index, 1); // remove the point at the index
						that.points = points;
						that.stage.update();
					}
				}
			});
			if (!_controls) that.hideControls();
			that.dispatchEvent("update");
		} // end of init()

		function getCurrentSelected() {
			var answer = [];
			var currentSet = that.selectionManager.currentSet;
			if (currentSet && currentSet.selections && currentSet.selections.length > 0) {
				for(var i=0; i<currentSet.selections.length; i++) {
					var point = that.pointObjects[currentSet.selections[i]];
					if (currentSet == that.selectedBalls) {
						answer.push(point[1]);
					} else if (currentSet == that.selectedRect1s) {
						answer.push(point[2]);
					}	else if (currentSet == that.selectedRect2s) {
						answer.push(point[3]);
					} else {
						continue;
					}
				}
			}
			return answer;
		}

		function replaceControls(target) {
			if (!that.selectPoints) return;
			// move ball back to origin and move set accordingly
			// so if we animate the set it will behave as expected
			if (target.type != "Circle") return;
			var ball = target;
			var set = ball.mySet;
			var rect1 = ball.rect1;
			var rect2 = ball.rect2;
			rect1.x -= ball.x;
			rect1.y -= ball.y;
			rect2.x -= ball.x;
			rect2.y -= ball.y;
			set.x += ball.x;
			set.y += ball.y;
			ball.x = 0;
			ball.y = 0;
		}

		that.selectionManager.on("keydown", function (e) {
			if (!that.selectPoints) return;
			if (!that.keyFocus) return;
			if (e.keyCode >= 37 && e.keyCode <= 40) {
				var currentSelected = getCurrentSelected();
				if (currentSelected.length > 0) {
					for(var i=0; i<currentSelected.length; i++) {
						var pointObj = currentSelected[i];
						if (e.keyCode == 37) pointObj.x -= that.selectionManager.shiftKey?10:1;
						else if (e.keyCode == 39) pointObj.x += that.selectionManager.shiftKey?10:1;
						else if (e.keyCode == 38) pointObj.y -= that.selectionManager.shiftKey?10:1;
						else if (e.keyCode == 40) pointObj.y += that.selectionManager.shiftKey?10:1;
						mapMove(pointObj);
					}
					drawShape();
					if (that.stage) that.stage.update();
				}
			}
		});

		that.selectionManager.on("keyup", function (e) {
			if (!that.selectPoints) return;
			if (!that.keyFocus) return;
			if (e.keyCode >= 37 && e.keyCode <= 40) {
				var currentSelected = getCurrentSelected();
				if (currentSelected.length > 0) {
					for(var i=0; i<currentSelected.length; i++) {
						replaceControls(currentSelected[i]);
					}
				}
			}
		});

		that.selectionManager.on("undo", function () {
			if (!that.selectPoints) return;
			if (!that.keyFocus) return;
			if (that.lastPoints) {
				var tempPoints = zim.copy(that.lastPoints);
				that.lastPoints = zim.copy(that.points);
				that.points = tempPoints;
				if (that.stage) that.stage.update()
			}
		});


		mapMove = function (target) {
			if (that.lockControls) return;
			if (target.rect1) { // pressmove on ball
				var ball = target;
				var diffX = ball.x - ball.startX;
				var diffY = ball.y - ball.startY;
				ball.rect1.x = ball.rect1.startX + diffX;
				ball.rect1.y = ball.rect1.startY + diffY;
				ball.rect2.x = ball.rect2.startX + diffX;
				ball.rect2.y = ball.rect2.startY + diffY;
			} else { // pressmove on control
				var rect = target;
				var other = rect.other; // the other handle
				var ball = rect.ball;
				var index = ball.index;
				var type = controlType;
				if (!zot(_points[index][4])) type = _points[index][4];
				if (type == "straight" || type == "mirror") {
					var dX = rect.x - ball.x;
					var dY = rect.y - ball.y;
					if (type == "mirror") {
						other.x = ball.x - dX;
						other.y = ball.y - dY;
					} else {
						var a = Math.atan2(dY, dX);
						var dNewX = -other.stickLength * Math.cos(a+Math.PI);
						var dNewY = -other.stickLength * Math.sin(a+Math.PI);
						other.x = ball.x - dNewX;
						other.y = ball.y - dNewY;
					}
				}
			}
		};

		Object.defineProperty(that, 'move', {
			get: function() {
				return move;
			},
			set: function(value) {
				if (move != value) {
					move = value;
					if (value) {
						startDragging();
					} else {
						stopDragging();
					}
				}
			}
		});

		Object.defineProperty(that, 'interactive', {
			get: function() {
				return interactive;
			},
			set: function(value) {
				interactive = value;
				that.showControls = interactive;
				that.allowToggle = interactive;
				that.editPoints = interactive;
				that.lockControls = !interactive; // note negative
				that.selectPoints = interactive;
				that.move = interactive;
				that.points = that.points; // force remake
			}
		});

		Object.defineProperty(that, 'allowToggle', {
			get: function() {
				return allowToggle;
			},
			set: function(value) {
				if (allowToggle != value) {
					allowToggle = value;
					if (allowToggle) {
						if (that.move) startDragging();
					} else {
						if (!_controls && that.move) stopDragging();
					}
				}
			}
		});

		function startDragging() {
			if (that.move=="always") return;
			if (draggingCheck) return;
			draggingCheck = true;
			shape.drag({onTop:false});
			moveDownEvent = shape.on("mousedown", moveDownEvent);
			movePressEvent = shape.on("pressmove", movePressEvent);
			moveUpEvent = shape.on("pressup", moveUpEvent);
		}
		function stopDragging(making) {
			if (that.move=="always") return;
			if (!making && !draggingCheck) return;
			draggingCheck = false;
			shape.noDrag();
			shape.off("mousedown", moveDownEvent);
			shape.off("pressmove", movePressEvent);
			shape.off("pressup", moveUpEvent);
		}

		Object.defineProperty(that, 'controlsVisible', {
			get: function() {
				return _controls;
			},
			set: function(value) {
				_controls = value;
				if (value) {
					that.showControls();
				} else {
					that.hideControls();
				}
			}
		});

		var _lockControls = lockControls;
		Object.defineProperty(that, 'lockControls', {
			get: function() {
				return _lockControls;
			},
			set: function(value) {
				_lockControls = value;
				if (value) {
					that.controls.mouseChildren = false;
					that.controls.mouseEnabled = false;
				} else {
					that.controls.mouseChildren = true;
					that.controls.mouseEnabled = true;
				}
			}
		});
		that.lockControls = _lockControls;

		Object.defineProperty(that, 'color', {
			get: function() {
				return _color;
			},
			set: function(value) {
				if (zot(value)) value = "black";
				_color = value;
				colorObj.style = _color;
			}
		});
		var startColor;
		var endColor;
		this.setColorRange = function(color1, color2) {
			if (zot(color2)) {
				startColor = that.color;
				endColor = color1;
			} else if (zot(color1)) {
				startColor = that.color;
				endColor = color2;
			} else {
				startColor = color1;
				endColor = color2;
			}
			return that;
		}
		var _colorRange = 0;
		Object.defineProperty(that, 'colorRange', {
			get: function() {
				return _colorRange;
			},
			set: function(value) {
				_colorRange = value;
				if (!zot(startColor) && !zot(endColor)) {
					that.color = zim.colorRange(startColor, endColor, value);
				}
			}
		});

		Object.defineProperty(that, 'borderColor', {
			get: function() {
				return _borderColor;
			},
			set: function(value) {
				if (zot(value)) return;
				_borderColor = value;
				if (!borderColorObj) drawShape();
				else borderColorObj.style = _borderColor;
			}
		});
		Object.defineProperty(that, 'borderWidth', {
			get: function() {
				return _borderWidth;
			},
			set: function(value) {
				if (zot(value)) return;
				if (!(value>0)) value = 0;
				_borderWidth = value;
				if (!borderWidthObj || _borderWidth == 0) drawShape();
				else {
					borderWidthObj.width = _borderWidth;
					if (dashed) {
						borderDashedObj.segments = [20, 10];
						borderDashedObj.offset = 5;
					}
				}
			}
		});

		if (typeof KEYFOCUS !== typeof undefined) zim.KEYFOCUS = KEYFOCUS;
		Object.defineProperty(this, 'keyFocus', {
			get: function() {
				return zim.KEYFOCUS == that;
			},
			set: function(value) {
				zim.KEYFOCUS = that;
			}
		});
		if (that.selectPoints && !zim.KEYFOCUS) setFocus();
		function setFocus() {that.keyFocus = true; var d=document.activeElement; if (d) d.blur();}


		Object.defineProperty(that, 'points', {
			get: function() {
				var points = [];
				var point; var p;
				for (var i=0; i<_points.length; i++) {
					p = _points[i];
					point = [
						zim.decimals(p[0].x),
						zim.decimals(p[0].y),
						zim.decimals(p[1].x),
						zim.decimals(p[1].y),
						zim.decimals(p[2].x),
						zim.decimals(p[2].y),
						zim.decimals(p[3].x),
						zim.decimals(p[3].y)
					];
					if (p[4] && p[4]!=="straight") point.push(p[4])
					points.push(point);
				}
				return points;
			},
			set: function(value) {
				that.dispose(true);
				points = checkForShape(value);

				if (that.shape) {
					that.shape.graphics.clear();
					that.sticks.graphics.clear();
					that.controls.noDrag();
					that.removeAllChildren();
					delete that.shape;
					delete that.sticks;
					delete that.controls;
				}
				init(); // remake Blob
				that.lockControls = _lockControls;
 			}
		});

		Object.defineProperty(that, 'pointsAdjusted', { // points with rotation
			get: function() {
				var points = [];
				var point; var p; var poo;
				var pObjects = that.pointObjects;
				zim.loop(pObjects.length, function(i, t) {
					po = pObjects[i];
					p = _points[i];
					if (po[0].rotation==0 && po[0].scaleX==0 && po[0].scaleY==0) { // get points
						point = [
							zim.decimals(p[0].x),
							zim.decimals(p[0].y),
							zim.decimals(p[1].x),
							zim.decimals(p[1].y),
							zim.decimals(p[2].x),
							zim.decimals(p[2].y),
							zim.decimals(p[3].x),
							zim.decimals(p[3].y)
						];
					} else {
						var lo1 = po[0].localToLocal(po[2].x, po[2].y, po[0].parent);
						var lo2 = po[0].localToLocal(po[3].x, po[3].y, po[0].parent);
						point = [
							zim.decimals(p[0].x),
							zim.decimals(p[0].y),
							zim.decimals(p[1].x),
							zim.decimals(p[1].y),
							zim.decimals(lo1.x-p[0].x),
							zim.decimals(lo1.y-p[0].y),
							zim.decimals(lo2.x-p[0].x),
							zim.decimals(lo2.y-p[0].y)
						];
					}
					if (p[4] && p[4]!=="mirror") point.push(p[4])
					points.push(point);
				});
				return points;
			},
			set: function(value) {
				if (zon) zog("Blob() - pointsAdjusted is read only")
			}
		});

		Object.defineProperty(that, 'pointObjects', {
			get: function() {
				return _points;
			},
			set: function(value) {
				if (zon) {zog("Blob() - pointObjects is read only - but its contents can be manipulated - use blob.update() after changes")}
			}
		});

		Object.defineProperty(that, 'pointControls', {
			get: function() {
				return _pointControls;
			},
			set: function(value) {
				if (zon) {zog("Blob() - pointControls is read only - but its contents can be manipulated - use blob.update() after changes")}
			}
		});

		Object.defineProperty(that, 'pointCircles', {
			get: function() {
				return _pointCircles;
			},
			set: function(value) {
				if (zon) {zog("Blob() - pointCircles is read only - but its contents can be manipulated - use blob.update() after changes")}
			}
		});

		// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// path manipulation and animating to path methods

		Object.defineProperty(that, 'segmentPoints', {
			get: function() {
				var p = that.pointsAdjusted;
				var array = []; // array of prepared segment points
				zim.loop(p.length, function(i, t) {
					var s = that.getSegmentPoint(p[i], p[i<t-1?i+1:0])
					array.push(s);
				});
				return array;
			},
			set: function(value) {
				if (zon) {zog("Blob() - segmentPoints is read only")}
			}
		});

		Object.defineProperty(that, 'segmentRatios', {
			get: function() {
				var distances = []
				var total = 0;
				zim.loop(that.segmentPoints, function(points) {
					var d = zim.distanceAlongCurve(points)
					distances.push(d);
					total += d;
				});
				var percents = [];
				var totalPercents = 0;
				zim.loop(distances, function (d) {
					var p = d/total;
					totalPercents += p;
					percents.push(totalPercents);
				});
				return percents;
			},
			set: function(value) {
				if (zon) {zog("Blob() - segmentRatios is read only")}
			}
		});

		that.getPointAngle = function(index) {
			var p = that.pointObjects[index][0]; // parent
			var r1 = that.pointObjects[index][2];
			var r2 = that.pointObjects[index][3];
			if (p==that.stage) {
				var globalR1 = new zim.Point(r1.x, r1.y);
				var globalR2 = new zim.Point(r2.x, r2.y);
			} else {
				var globalR1 = p.localToGlobal(r1.x, r1.y);
				var globalR2 = p.localToGlobal(r2.x, r2.y);
			}
			return zim.angle(globalR1.x, globalR1.y,globalR2.x, globalR2.y);
		}

		that.getSegmentPoint = function(point1, point2) {
			if (zot(point1) || zot(point2)) return;
			// dragging points temporarily puts data out of order
			if (point1[2] != 0 || point1[3] != 0) {
				point1[4] -= point1[2];
				point1[5] -= point1[3];
				point1[6] -= point1[2];
				point1[7] -= point1[3];
				point1[0] += point1[2];
				point1[1] += point1[3];
				point1[2] = 0;
				point1[3] = 0;
			}
			if (point2[2] != 0 || point2[3] != 0) {
				point2[4] -= point2[2];
				point2[5] -= point2[3];
				point2[6] -= point2[2];
				point2[7] -= point2[3];
				point2[0] += point2[2];
				point2[1] += point2[3];
				point2[2] = 0;
				point2[3] = 0;
			}
			var p1 = {x:point1[0], y:point1[1]};
			var p2 = {x:point1[0]+point1[6], y:point1[1]+point1[7]};
			var p3 = {x:point2[0]+point2[4], y:point2[1]+point2[5]};
			var p4 = {x:point2[0], y:point2[1]};
			if (sets.x != 0 || sets.y !=0) {
				p1.x+=sets.x;
				p2.x+=sets.x;
				p3.x+=sets.x;
				p4.x+=sets.x;
				p1.y+=sets.y;
				p2.y+=sets.y;
				p3.y+=sets.y;
				p4.y+=sets.y;
			}
			return [p1,p2,p3,p4];
		}

		that.getAdjacentSegmentData = function(index) {
			if (zot(index)) index = 0;
			var p = that.pointsAdjusted;
			if (that.num == 2) {
				return [
					[that.getSegmentPoint(p[0], p[1]),
					that.getSegmentPoint(p[1], p[0])],
					[0,1]
				];
			}
			if (index == 0) {
				return [
					[that.getSegmentPoint(p[that.num-1], p[0]),
					that.getSegmentPoint(p[0], p[1]),
					that.getSegmentPoint(p[1], p[2])],
					[that.num-1,0,1]
				];
			} else if (index >= that.num-1) {
				return [
					[that.getSegmentPoint(p[that.num-2], p[that.num-1]),
					that.getSegmentPoint(p[that.num-1], p[0]),
					that.getSegmentPoint(p[0], p[1])],
					[that.num-2,that.num-1,0]
				];
			} else {
				var lastIndex = (index+2>=that.num)?0:index+2
				return [
					[that.getSegmentPoint(p[index-1], p[index]),
					that.getSegmentPoint(p[index], p[index+1]),
					that.getSegmentPoint(p[index+1], p[lastIndex])],
					[index-1,index,index+1]
				];
			}
		}

		that.getCurvePoint = function(ratio, segmentRatios, segmentPoints, getAngle) {
			if (zot(segmentRatios)) segmentRatios = that.segmentRatios;
			if (zot(segmentPoints)) segmentPoints = that.segmentPoints;
			if (zot(getAngle)) getAngle = false;
			var percents = segmentRatios;
			var segments = segmentPoints;
			var afterIndex = zim.loop(percents, function (p, i) {
				if (p >= ratio) return i;
			});
			var earlierPercent = afterIndex > 0 ? percents[afterIndex-1] : 0;
			var localTotal = afterIndex > 0 ? (percents[afterIndex]-percents[afterIndex-1]):percents[afterIndex];
			var localPercent = (ratio-earlierPercent)/localTotal;
			var finalPoint = zim.pointAlongCurve(segments[afterIndex], localPercent, getAngle);
			if (zot(finalPoint)) return undefined;
			var finalFinalPoint = that.localToGlobal(finalPoint.x, finalPoint.y);
			finalFinalPoint.angle = finalPoint.angle;
			finalFinalPoint.z = afterIndex;
			return !zot(finalFinalPoint) ? finalFinalPoint : undefined;
		}

		function proportion(p1, p2, ratio) {
			return {
				x:p1.x + (p2.x-p1.x)*ratio,
				y:p1.y + (p2.y-p1.y)*ratio
			}
		}
		function insertPointData(points, controls, ratios, percent, controlType, skipPoint, dataOnly) {
			var index = points.length-1; // adjust for squiggle
			var lastRatio = 0;
			var currentRatio = 0;
			if (percent == 100 && that.type == "Squiggle") percent = 99.99;
			percent = (percent+100000)%100;

			zim.loop(ratios, function (ratio, i) {
				if (percent/100 < ratio) {
					index = i;
					currentRatio = ratio;
					return true;
				}
				lastRatio = ratio;
			});
			var segment = that.segmentPoints[index];
			var r = currentRatio > 0?(percent/100-lastRatio)/(currentRatio-lastRatio):0
			// zog(percent)
			// zog(percent/100-currentRatio/ratios.length)
			// var r = currentRatio > 0?(percent/100-1/ratios.length*currentRatio)/(currentRatio-lastRatio):0

			var point = zim.pointAlongCurve(segment, r)
			var newPoint = [point.x,point.y, 0, 0];
			if (skipPoint) return;
			if (dataOnly) {
				that.interpolatedPoints.push({x:point.x, y:point.y, r:percent/100});
				return;
			}
			if (controlType != "none") {
				// calculate new handles and adjust old handles
				// [controlX, controlY, circleX, circleY, rect1X, rect1Y, rect2X, rect2Y, controlType]
				var startHandle = proportion(segment[0], segment[1], r);
				var midPoint = proportion(segment[1], segment[2], r);
				var endHandle = proportion(segment[2], segment[3], r);
				var newStartHandle = proportion(startHandle, midPoint, r);
				var newEndHandle = proportion(midPoint, endHandle, r);
				newPoint[4] = newStartHandle.x-point.x
				newPoint[5] = newStartHandle.y-point.y;
				newPoint[6] = newEndHandle.x-point.x;
				newPoint[7] = newEndHandle.y-point.y;
				var start = that.localToLocal(startHandle.x, startHandle.y, controls[index])
				points[index][6] = start.x;
				points[index][7] = start.y;
				var end = that.localToLocal(endHandle.x, endHandle.y, controls[(index+1)%points.length])
				points[(index+1)%points.length][4] = end.x;
				points[(index+1)%points.length][5] = end.y;
			}
			if (controlType) newPoint[8] = controlType;
			points.splice(index+1, 0, newPoint);
		}

		this.addPoint = function(percent, controlType) {
			if (zot(percent)) percent = 100;
			var points = that.points;
			var ratios = that.segmentRatios;
			var controls = that.pointControls;
			insertPointData(points, controls, ratios, percent, controlType);
			that.points = points;
			that.num = points.length;
			return that;
		}

		this.addPoints = function(num, controlType, startPoint, spread, dataOnly, points) {
			if (zot(points)) points = zim.copy(that.points);
			var ratios = zim.copy(that.segmentRatios);
			var lastRatio = 0;

			if (dataOnly) that.interpolatedPoints = [];

			// dataOnly should add points to current point too
			// but can't just use current point because sometimes that is static
			// like when dragging the shape or a point - it does not register until mouseup
			// and things like hitTestPath need that to be dynamic
			// So the below does not work:
			// if (dataOnly) {
			// 	that.interpolatedPoints = [];
			// 	zim.loop(points, function (point, i) {
			// 		if (!zot(startPoint) && i!=startPoint) return;
			// 		that.interpolatedPoints.push({x:point[0], y:point[1]})
			// 	});
			// }
			if (spread) var totalPoints = ratios.length*num;
			zim.loop(ratios, function (ratio, j) {
				if (dataOnly) insertPointData(points, that.pointControls, that.segmentRatios, lastRatio*100, controlType, !zot(startPoint) && j!=startPoint, dataOnly);
				var numCount = spread?Math.round(totalPoints*(ratio-lastRatio)):num
				var div = 1/(numCount+1);
				zim.loop(numCount, function(i) {
					var r = lastRatio + (ratio-lastRatio)*div*(i+1);
					insertPointData(points, that.pointControls, that.segmentRatios, r*100, controlType, !zot(startPoint) && j!=startPoint, dataOnly);
					if (!dataOnly && num > 0) that.points = points;
				});
				lastRatio = ratio;
			});
			if (dataOnly && that.type == "Squiggle") insertPointData(points, that.pointControls, that.segmentRatios, 100, controlType, null, dataOnly);
			if (that.stage) that.stage.update();
			that.num = points.length;
			return that;
		}
		this.interpolate = function(num, startPoint, spread, points) {
			if (zot(num)) num = 1;
			// dataOnly will add Point to start point too
			that.addPoints(num, "none", startPoint, spread, true, points)
			return that.interpolatedPoints;
		}

		this.linearGradient = function(colors,ratios,x0,y0,x1,y1) {
			this.linearGradientParams = Array.prototype.slice.call(arguments);
			this.colorCommand.linearGradient(colors,ratios,x0,y0,x1,y1);
			return this;
		}
		this.radialGradient = function(colors,ratios,x0,y0,radius0,x1,y1,radius1) {
			this.radialGradientParams = Array.prototype.slice.call(arguments);
			this.colorCommand.radialGradient(colors,ratios,x0,y0,radius0,x1,y1,radius1);
			return this;
		}

		this.dispose = function(temp) {
			// if (that.toggleStageEvent) that.stage.off("stagemousedown", that.toggleStageEvent);
			// this.zimContainer_dispose();
			// return true
			if (!that.shape) return;
			that.shape.cursor = "default";
			for (var i=0; i<that.pointObjects.length; i++) {
				that.pointObjects[i][1].removeAllEventListeners();
			}
			for (i=0; i<_pointCircles.length; i++) {
				_pointCircles[i].removeAllEventListeners();
			}
			that.sticks.removeFrom(that);
			that.controls.removeFrom(that);
			that.shape.removeAllEventListeners();
			that.controls.removeAllEventListeners();
			that.off("mousedown", that.toggleEvent);
			that.off("click", that.clickEvent);
			if (that.toggleStageEvent) that.stage.off("stagemousedown", that.toggleStageEvent);
			if (!temp && that.selectPoints) that.selectionManager.removeAllEventListeners();
			return true;
		}
	}
	zim.extend(zim.Blob, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-53.5

// SUBSECTION COMPONENTS

/*--
zim.Label = function(text, size, font, color, rollColor, shadowColor, shadowBlur, align, valign, lineWidth, lineHeight, fontOptions, backing, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, group, inherit)

Label
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Makes a label - wraps the createjs Text object.
Can use with Button, CheckBox, RadioButtons and Pane.
Text seems to come in different sizes so we do our best.
Have tended to find that left and alphabetic are most consistent across browsers.
Custom fonts loaded through css can be used as well.
NOTE: can wrap text at given width using lineWidth parameter.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var label = new Label("Hello");
label.center(stage); // adds label to and centers on the stage

var label = new Label({
	text:"CLICK",
	size:100,
	font:"courier",
	color:"white",
	rollColor:"red",
	fontOptions:"italic bold"
});
stage.addChild(label);
label.x = label.y = 100;
label.on("click", function(){zog("clicking");});
END EXAMPLE

EXAMPLE
STYLE = {font:"courier"};
new Label("Hi Courier").center(); // will be courier not arial

STYLE = {text:"YAY!", color:"Red"};
new Label().center().mov(0,100); // will say YAY! in red arial font
new Label("Hello").center().mov(0,200); // will say Hello in red arial
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function

text |ZIM VEE| - String for the the text of the label
size - (default 36) the size of the font in pixels
font - (default arial) the font or list of fonts for the text
color - (default "black") color of font (any CSS color)
rollColor - (default color) the rollover color of the font
shadowColor - (default -1) for no shadow - set to any css color to see
shadowBlur - (default 14) if shadow is present
align - ((default "left") text registration point alignment also "center" and "right"
valign - (default "center") vertical registration point alignment alse "middle / center", "bottom"
lineWidth - (default false) for no wrapping (use \n) Can set to number for wrap
lineHeight - (default getMeasuredLineHeight) set to number to adjust line height
fontOptions - (default null) css VALUES as a single string for font-style font-variant font-weight
	eg. "italic bold small-caps" or just "italic", etc.
backing - (default null) a Display object for the backing of the label (eg. Shape, Bitmap, Container, Sprite)
	see ZIM Pizzazz module for a fun set of Shapes like Boomerangs, Ovals, Lightning Bolts, etc.
outlineColor - (default null - or black if outlineWidth set) - the color of the outline of the text
outlineWidth - (default null - or (size*.2) if outlineColor set) - the thickness of the outline of the text
backgroundColor - (default null) set to CSS color to add a rectangular color around the label
	The background color will change size to match the text of the label
	Note: the backgroundColor is different than a backing which can be any Display Object
	and background parameters are ignored if a backing parameter is set
backgroundBorderColor - (default null) the background stroke color
backgroundBorderWidth - (default null) thickness of the background border
corner - (default 0) the round of corner of the background if there is one
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
backgroundDashed - (default null) set to true for dashed background border (if backgroundBorderColor or backgroundBorderWidth set)
padding - (default 10 if backgroundColor set) places the border this amount from text (see paddingHorizontal and paddingVertical)
	padding parameters are ignored if there is no backgroundColor set (also ignored if a backing parameter is set)
paddingHorizontal - (default padding) places border out at top bottom
paddingVertical - (default padding) places border out at left and right
shiftHorizontal - (default 0) move the label (CreateJS Text) inside the Label container horizontally
shiftVertical - (default 0) move the label (CreateJS Text) inside the Label container vertically
rollPersist - (default false) set to true to maintain rollover stage as long as mousedown or press is activated (used by Buttons)
labelWidth - (default null) the same as the lineWidth - the text will wrap at the labelWidth (added to match labelHeight)
labelHeight - (default null) the height of the text - setting this will probably alter the font size - so the size parameter is overwritten
	for labelHeight to work, the labelWidth must also be set
	using labelWidth and labelHeight together allow you to fit as much text into specified width and height dimensions
maxSize - (default null) set to limit the font size when using labelWidth and labelHeight
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
setColorRange(color1, color2) - set a color range for label - used by colorRange property - returns obj for chaining
	if one color is used, the current color is used and color1 is the second color in the range
showRollColor(visible) - default true to show roll color (used internally)
cache(see Container docs for parameter description) - overrides CreateJS cache() and returns object for chaining
	Leave parameters blank to cache bounds of shape (plus outer edge of border if borderWidth > 0)
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
label - references the text object of the label
text - references the text property of the text object
size - the font size of the Label (without px)
paddingHorizontal - read-only value for paddingHorizontal of label
	note - no padding property - that gets split into paddingHorizontal and paddingVertical
paddingVertical - read-only value for paddingVertical of label
color - gets or sets the label text color
colorRange - if setColorRange() is used, the colorRange is a ratio (0-1) between the colors
	setting the colorRange will change the color property of the label
	for instance, label.setColorRange(blue, pink) then label.colorRange = .5
	will set the color of the label to half way between blue and pink
	label.animate({color:red}, 1000); is a shortcut to animate the colorRange
	label.wiggle("colorRange", .5, .2, .5, 1000, 5000) will wiggle the colorRange
rollColor - gets or sets the label rollover color
labelWidth - the width at which the text wraps
labelHeight - setting this and labelWidth will change the font size to fit within the specified dimensions
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
depth - for ZIM VR - the depth used to shift left and right channel and for parallax in VR - also see dep() ZIM Display method
backing - access to backing object
background - access to background Rectangle if backgroundColor is set
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+54
	zim.Label = function(text, size, font, color, rollColor, shadowColor, shadowBlur, align, valign, lineWidth, lineHeight, fontOptions, backing, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, group, inherit) {
		var sig = "text, size, font, color, rollColor, shadowColor, shadowBlur, align, valign, lineWidth, lineHeight, fontOptions, backing, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, group, inherit";
		var duo; if (duo = zob(zim.Label, arguments, sig, this)) return duo;
		z_d("54");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Label";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(text)) text=DS.text!=null?DS.text:"LABEL";
		var originalText = text;
		text = zim.Pick.choose(text);
		var emptyText = false;
		if (text === "") {
			text = " ";
			emptyText = true;
		}
		if (zot(size)) size=DS.size!=null?DS.size:36;
		if (zot(font)) font=DS.font!=null?DS.font:"arial"; // arial unless manually set
		if (zot(color)) color=DS.color!=null?DS.color:"black";
		if (zot(rollColor)) rollColor=DS.rollColor!=null?DS.rollColor:color;
		if (zot(shadowColor) || shadowColor=="ignore") shadowColor=(DS.shadowColor!=null&&shadowColor!="ignore")?DS.shadowColor:-1;
		if (zot(shadowBlur) || shadowBlur=="ignore") shadowBlur=(DS.shadowBlur!=null&&shadowBlur!="ignore")?DS.shadowBlur:14;
		if (zot(align)) align=DS.align!=null?DS.align:"left";
		if (zot(valign)) valign=DS.valign!=null?DS.valign:"top";
		if (zot(fontOptions)) fontOptions=DS.fontOptions!=null?DS.fontOptions:"";
		if ((!zot(outlineColor) || !zot(DS.outlineColor)) && zot(outlineWidth)) outlineWidth = DS.outlineWidth!=null?DS.outlineWidth:Math.round(size*.2);
		if ((!zot(outlineWidth) || !zot(DS.outlineWidth)) && zot(outlineColor)) outlineColor = DS.outlineColor!=null?DS.outlineColor:"#000000";
		if (zot(outlineWidth)) outlineWidth = DS.outlineWidth!=null?DS.outlineWidth:0;
		if (zot(backgroundColor) || backgroundColor=="ignore") backgroundColor = (DS.backgroundColor!=null&&backgroundColor!="ignore")?DS.backgroundColor:null;
		if (zot(padding) || padding=="ignore") padding = (DS.padding!=null&&padding!="ignore")?DS.padding:10;
		if (zot(paddingHorizontal)) paddingHorizontal = DS.paddingHorizontal!=null?DS.paddingHorizontal:padding;
		if (zot(paddingVertical)) paddingVertical = DS.paddingVertical!=null?DS.paddingVertical:padding;
		if (zot(shiftHorizontal)) shiftHorizontal = DS.shiftHorizontal!=null?DS.shiftHorizontal:0;
		if (zot(shiftVertical)) shiftVertical = DS.shiftVertical!=null?DS.shiftVertical:0;
		if (zot(lineWidth)) lineWidth = DS.lineWidth!=null?DS.lineWidth:null;
		if (zot(lineHeight)) lineHeight = DS.lineHeight!=null?DS.lineHeight:null;
		if (zot(backing) || backing=="ignore") backing = (DS.backing!=null&&backing!="ignore")?DS.backing.clone():null;
		if (zot(rollPersist)) DS.rollPersist!=null?DS.rollPersist:false;
		if (DS.labelWidth!=null) lineWidth = DS.labelWidth;
		if (!zot(labelWidth)) lineWidth = labelWidth;
		if (align == "middle") align = "center";
		if (zot(maxSize)) DS.maxSize!=null?DS.maxSize:null;
		size = maxSize?Math.min(size, maxSize):size;

		var retina = (typeof zimDefaultFrame!="undefined"&&zimDefaultFrame.retina);

		var that = this;
		this.mouseChildren = false;
		this.paddingVertical = paddingVertical;
		this.paddingHorizontal = paddingHorizontal;

		var obj = this.label = new createjs.Text(String(text), fontOptions + " " + size + "px " + font, color);
		obj.textAlign = align;
		obj.lineWidth = lineWidth;
		obj.lineHeight = lineHeight;
		obj.textBaseline = "alphabetic";
		if (outlineWidth > 0) {
			var obj2 = this.outlineLabel = obj.clone();
			obj2.color = outlineColor;
			obj2.outline = outlineWidth;
		}
		if (shadowColor != -1 && shadowBlur > 0) obj.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
		this.addChild(obj);

		function setSize() {
			var b = obj.getBounds();
			var yAdjust;
			if (valign == "baseline") {
				yAdjust = b.y;
			} else if (valign == "top") {
				obj.y = size-size/6;
				if (!retina && obj2) obj2.y = size-size/6;
				yAdjust = 0;
			} else if (valign == "center" || valign == "middle") {
				yAdjust = - b.height / 2;
				obj.y = size*.3;
				if (!retina && obj2) obj2.y = size*.3;
			} else { // bottom align
				yAdjust = -b.height;
			}
			if (backing) {
				if (backing.type == "Pattern") {
					that.setBounds(b.x-paddingHorizontal, yAdjust-paddingVertical, b.width+paddingHorizontal*2, b.height+paddingVertical*2);
				} else {
					var bb = backing.getBounds();
					that.setBounds(bb.x, bb.y, bb.width, bb.height);
				}
			} else if (!zot(backgroundColor)) {
				that.setBounds(b.x, yAdjust, b.width, b.height);
				that.removeChild(that.background);
				that.background = new zim.Rectangle(
					that.getBounds().width+paddingHorizontal*2, that.getBounds().height+paddingVertical*2,
					backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, null, false
				);
				zim.center(that.background, that, 0);
				that.setBounds(that.background.x, that.background.y, that.background.width, that.background.height);
			} else {
				that.setBounds(b.x, yAdjust, b.width, b.height);
				hitArea.graphics.c().f("black").r(that.getBounds().x, that.getBounds().y, that.getBounds().width, that.getBounds().height);
			}
			zim.center(obj, that);
			// zim.pos(obj, (align=="left"||align=="right")?(backing||that.background?paddingHorizontal:0):null, (valign=="top"||valign=="baseline"||valign=="bottom")?(backing||that.background?paddingVertical:0):null, align=="right", valign=="bottom");
			zim.pos(obj, (align=="left"||align=="right")?(backing||that.background?paddingHorizontal:0):null, backing||that.background?paddingVertical:0, align=="right", valign=="bottom");
			if (valign != "baseline") obj.y += size/(typeof zimDefaultFrame!="undefined"&&zimDefaultFrame.retina!=true?32:16); //32; // backing often on capital letters without descenders - was /16
			obj.x += shiftHorizontal;
			obj.y += shiftVertical;
			if (obj2) {
				if (retina) {
					that.added(function(stage){
						// CREATEJS NEEDS TO FIX their GLOBALTOLOCAL, etc. to work with scaled stage
						var b = obj2.getBounds();
						obj2.visible = true;
						// ** problem here - bounds should be around Label - not taking on lineHeight
						// because valign no longer works!
						var shim = new zim.Container(b.x, b.y, b.width, b.height);
						shim.addTo(that)
						shim.addChild(obj2);
						shim.center(that, that.numChildren-2);
						if (valign != "baseline") shim.y += size/16;
						shim.x += shiftHorizontal;
						shim.y += shiftVertical;

						stage.update();
					});
				} else {
					that.addChild(obj2);
					zim.center(obj2, that, that.numChildren-2);
					if (valign != "baseline") obj2.y += size/32;
					obj2.x += shiftHorizontal;
					obj2.y += shiftVertical;
					obj2.visible = true;
				}
			}
		}
	 	if (zot(backing) && zot(backgroundColor)) {
			var hitArea = new createjs.Shape();
			that.hitArea = hitArea;
		}
		setSize();

		if (!zot(backing)) {
			if (backing.type == "Pattern") {
				that.backing = new zim.Container(that.width, that.height, null, null, false).centerReg(null, null, false);
				if (shadowColor != -1 && shadowBlur > 0) {
					var shadowRect = new zim.Rectangle(that.width-2, that.height-2, "#666", null, null, corner, null, null, false).center(that.backing);
					shadowRect.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
				}
				var mask = that.backing.mask = new zim.Rectangle(that.width, that.height, backgroundColor, null, null, corner, null, null, false).addTo(that.backing);
				backing.centerReg(mask);
				backing.setMask(mask.shape);
				that.backing.pattern = backing;
			} else {
				that.backing = backing;
			}
			that.backing.center(that, 0);
			backing = that.backing;
		}

		Object.defineProperty(that, 'text', {
			get: function() {
				var t = (obj.text == " " && emptyText) ? "" : obj.text;
				return t;
			},
			set: function(value) {
				emptyText = false;
				if (value === "") {
					value = " ";
					emptyText = true;
				}
				obj.text = String(value);
				if (obj2) obj2.text = String(value);
				setSize();
				if (!zot(lineWidth) && !zot(labelHeight)) {
					fitText();
				}
			}
		});

		Object.defineProperty(that, 'size', {
			get: function() {
				return size;
			},
			set: function(value) {
				size = maxSize?Math.min(value, maxSize):value;
				var text = this.label.font;
				// var reg = text.match(/^(\D*|.*\s)(\d\.?\d*)+px(.*)$/i);
				var reg = text.match(/^(.*\s)(\d*\.?\d*)+px(.*)$/i);
				if (!reg) return;
				this.label.font = reg[1] + value + "px" + reg[3];
				if (obj2) obj2.font = reg[1] + value + "px" + reg[3];
				setSize();
			}
		});

		Object.defineProperty(that, 'color', {
			get: function() {
				return color;
			},
			set: function(value) {
				if (rollColor == color) rollColor = value;
				color = value;
				obj.color = color;
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});
		var startColor;
		var endColor;
		this.setColorRange = function(color1, color2) {
			if (zot(color2)) {
				startColor = that.color;
				endColor = color1;
			} else if (zot(color1)) {
				startColor = that.color;
				endColor = color2;
			} else {
				startColor = color1;
				endColor = color2;
			}
			return that;
		}
		var _colorRange = 0;
		Object.defineProperty(that, 'colorRange', {
			get: function() {
				return _colorRange;
			},
			set: function(value) {
				_colorRange = value;
				if (!zot(startColor) && !zot(endColor)) {
					that.color = zim.colorRange(startColor, endColor, value);
				}
			}
		});

		Object.defineProperty(that, 'outlineColor', {
			get: function() {
				return outlineColor;
			},
			set: function(value) {
				outlineColor = value;
				if (obj2) obj2.color = outlineColor;
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		Object.defineProperty(that, 'rollColor', {
			get: function() {
				return rollColor;
			},
			set: function(value) {
				rollColor = value;
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
				obj.color = color;
				that.mouseChildren = false;
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		this.showRollColor = function(yes) {
			if (zot(yes)) yes = true;
			if (yes) {
				obj.color = rollColor;
			} else {
				obj.color = color;
			}
			if (that.stage) that.stage.update();
			return that;
		}

		this.mouseoverEvent = this.on("mouseover", function(e) {if (that.showRollColor) that.showRollColor();});
		this.mouseoutEvent = this.on("mouseout", function(e) {if (!that.rollPersist) that.showRollColor(false);});
		this.pressupEvent = this.on("pressup", function(e) {if (that.rollPersist) that.showRollColor(false);});

		Object.defineProperty(that, 'labelWidth', {
			get: function() {
				return lineWidth;
			},
			set: function(value) {
				if (value > 0) {
					lineWidth = value;
					that.label.lineWidth = value;
				}
				if (labelHeight) fitText();
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		Object.defineProperty(that, 'labelHeight', {
			get: function() {
				return labelHeight;
			},
			set: function(value) {
				if (value > 0) labelHeight = value;
				if (lineWidth) fitText();
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});
		if (!zot(lineWidth) && !zot(labelHeight)) {
			fitText();
		}
		function fitText() {
			that.size = 200;
			while(that.height > labelHeight || that.width > lineWidth) {
				that.size = that.size/2;
			}
			var count = 0;
			while(that.height <= labelHeight && that.width <= lineWidth) {
				count++;
				that.size = Math.ceil(that.size + 1);
				if (count>50) break;
			}
			that.size = that.size - 1;
		}

		zimStyleTransforms(this, DS)
		this.clone = function(exact) {
			return that.cloneProps(new zim.Label(exact?that.text:originalText, size, font, color, rollColor, shadowColor, shadowBlur, align, valign, lineWidth, lineHeight, fontOptions,
				!zot(backing)?backing.clone():null, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, this.group, inherit));
		}
	}
	zim.extend(zim.Label, zim.Container, "clone", "zimContainer");
	//-54


/*--
zim.LabelOnPath = function(label, path, percentAngle, percents, showPath, allowToggle, interactive, onTop, style, group, inherit)

LabelOnPath
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Makes a label along a path of a Squiggle or Blob - which can be interactive, fixed, toggled or hidden
A list of percentages for where the letters are can be provided to move around letters
See: https://zimjs.com/explore/labelonpath.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var lop = new LabelOnPath({
	label:"Hello World",
	// label:new Label({text:"JELLO JIGGLES!", size:50}),
	// path:new Blob(),
	// path:new Squiggle({
	//     color:lighter,
	//     thickness:4,
	//     points:[[0,75,0,0,-100,200,100,-200],[300,75,0,0,-100,200,100,-200]]
	// }).transformPoints("scaleX",2).transformPoints("rotation",0),
	percentAngle:100, // default
	showPath:false, // default
	allowToggle:true, // default
	interactive:true, // default
	onTop:false // default
}).center();
zog(lop.text)

frame.on("keydown", function () {
	// shows percent spacing of letters along path
	// could pass [results] in as an array to percents parameter of LabelOnPath
    zog(lop.percents.toString());
	// uncomment to record the path
	// could pass this in as the points parameter to start with a given path
    // lop.path.recordPoints(true);
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
label - (default "Label on Path") a String or a ZIM Label
	can set any label properties such as color, size, font, etc. on the label passed in
path - (default new Squiggle()) a ZIM Squiggle or ZIM Blob
	can set any properties such as color, points, etc. on the shape passed in
percentAngle - (default 100) from 0-100 (or beyond in either direction) as to how much to tilt the letters
percents - (default null) an array of percentage locations of the letters along the line - should match number of letters
showPath - (default true) Boolean to show path at start
allowToggle - (default true) Boolean to allow user to toggle path off and on
interactive - (default true) Boolean to allow user to change path with controls, drag or add and remove points
	can also set these individually on the path
onTop - (default false) - Boolean to set path on top when dragged
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
toggle(state) - leave off state to toggle path to opposite state.  Use true to hide and false to show - returns object for chaining
hidePath() - hides path - returns object for chaining
showPath() - shows path - returns object for chaining
resize() - if not interactive, call this to update the text on the path - returns object for chaining
cache(see Container docs for parameter description) - overrides CreateJS cache() and returns object for chaining
	Leave parameters blank to cache bounds of shape (plus outer edge of border if borderWidth > 0)
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - the name of the class as a String
text - get or set the text on the path
path - read-only access to path - but can manipulate the path
letters - access to ZIM Container of labels used for letters
	for instance labels.loop(function (label) {label.color = red;});
	or animate as a sequence labels.animate({props:{scale:1.5}, loop:true, rewind:true, sequence:200});
numLetters - how many letters - same as letters.numChildren
percents - access to the array of percents for letter positioning - resize() after changing unless interactive which auto resizes
allowToggle - get or set the Boolean to allow toggling the path
interactive - get or set the Boolean to allow interaction with the path
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object


ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+54.5

zim.LabelOnPath = function(label, path, percentAngle, percents, showPath, allowToggle, interactive, onTop, style, group, inherit) {
	var sig = "label, path, percentAngle, percents, showPath, allowToggle, interactive, onTop, style, group, inherit";
	var duo; if (duo = zob(zim.LabelOnPath, arguments, sig, this)) return duo;
	z_d("54.5");
	this.zimContainer_constructor(null,null,null,null,false);
	this.type = "LabelOnPath";
	this.group = group;
	var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

	if (zot(label)) label = DS.label!=null?DS.label:new zim.Label("Label on Path");
	if (zot(path)) path = DS.path!=null?DS.path:new zim.Squiggle({points:[[0,0,0,0,-86,57,86,-57],[300,150,0,0,-133,21,133,-21]]});
	if (zot(percentAngle)) percentAngle = DS.percentAngle!=null?DS.percentAngle:100;
	if (zot(percents)) percents = DS.percents!=null?DS.percents:null;
	if (zot(showPath)) showPath = DS.showPath!=null?DS.showPath:true;
	if (zot(allowToggle)) allowToggle = DS.allowToggle!=null?DS.allowToggle:true;
	if (zot(interactive)) interactive = DS.interactive!=null?DS.interactive:true;
	if (zot(onTop)) onTop = DS.onTop!=null?DS.onTop:false;

	path.addTo(this);
	var that = this;
	this.path = path;
	this.allowToggle = allowToggle;
	path.interactive = interactive;

	if (typeof label == "string") label = new zim.Label(label);

	var lastAlpha = path.alpha;
	if (!showPath) path.alp(0);
	path.onTop = onTop;

	var letters = this.letters = new zim.Container().addTo(this);
	if (!percents) {
		percents = [];
		for (var i=0; i<label.text.length; i++) {
			percents.push(zim.decimals(1/(label.text.length-(path.type=="Blob"?0:1))*100*i));
		}
	}
	this.percents = percents;

	function setText() {
		for (var i=letters.numChildren-1; i>=0; i--) {
			letters.getChildAt(i).dispose();
		}
		that.numLetters = label.text.length;
		for (var i=0; i<that.numLetters; i++) {
			var letter = label.clone();
			letter.text = label.text[i];
			letter.centerReg(letters).reg(null,letter.height)
			if (letter.text != "" && letter.text != " ") letter.expand(0);
			if (that.allowToggle) letter.cursor = "pointer";
			letter.on("mousedown", function () {
				if (!that.allowToggle) return;
				that.toggle();
			})
		}
		that.resize();
	}

	this.resize = function() {
		var segmentRatios = path.segmentRatios;
		var segmentPoints = path.segmentPoints;
		for (var i=0; i<this.numLetters; i++) {
			var point = path.getCurvePoint(percents[i]/100, segmentRatios, segmentPoints, true);
			if (!point) continue;
			var locPoint = this.globalToLocal(point.x, point.y);
			if (!locPoint) continue;
			letters.getChildAt(i)
				.loc(locPoint)
				.rot((point.angle>180?(point.angle-360):point.angle)*percentAngle/100);
		}
		return this;
	}

	setText();

	this.showPath = function(controls) {
		this.toggle(true);
		path.toggle(controls)
		return this;
	}
	this.hidePath = function() {
		this.toggle(false);
		return this;
	}

	Object.defineProperty(that, 'text', {
		get: function() {
			return label.text;
		},
		set: function(value) {
			label.text = value;
			percents = [];
			for (var i=0; i<label.text.length; i++) {
				percents.push(zim.decimals(1/(label.text.length-(path.type=="Blob"?0:1))*100*i));
			}
			setText();
			// if (that.stage) that.stage.update();
		}
	});

	Object.defineProperty(that, 'interactive', {
		get: function() {
			return interactive;
		},
		set: function(value) {
			interactive = value;
			path.interactive = value;
			if (this.ticker) zim.Ticker.remove(this.ticker);
			if (interactive) {
				this.ticker = zim.Ticker.add(function () {
					that.resize();
				});
			}
		}
	});

	if (this.interactive) {
		this.ticker = zim.Ticker.add(function () {
			that.resize();
		});
	}
	var _toggled = that.toggled = showPath;

	this.toggle = function(state) {
		if (!this.allowToggle) return;

		if (zot(state)) _toggled = !_toggled;
		else if (state) _toggled = true;
		else _toggled = false;
		if (_toggled) {
			path.alp(lastAlpha);
			if (this.interactive) {
				path.showControls();
				letters.mouseEnabled = false;
				letters.mouseChildren = false;
				letters.cursor = "default";
				if (this.controlHideEvent) path.off("controlshide", this.controlHideEvent);
				this.controlHideEvent = path.on("controlshide", function () {
					letters.mouseEnabled = true;
					letters.mouseChildren = true;
					letters.cursor = "pointer";
					lastAlpha = path.alpha;
					path.alp(0);
					_toggled = false;
					that.toggled = _toggled;
				}, null, true); // just once
			}
		} else {
			lastAlpha = path.alpha;
			path.alp(0);
			letters.mouseEnabled = true;
			letters.mouseChildren = true;
			letters.cursor = "pointer";
		}
		that.toggled = _toggled;
		that.stage.update();
		return that;
	}

	zimStyleTransforms(this, DS)
	this.clone = function() {
		return that.cloneProps(new zim.LabelOnPath(that.label.clone(), that.path.clone(), percentAngle, zim.copy(percents), showPath, allowToggle, interactive, onTop, style, this.group, inherit));
	}
	this.dispose = function() {
		if (this.ticker) zim.Ticker.remove(this.ticker);
		this.zimContainer_dispose();
		return true;
	}
}
zim.extend(zim.LabelOnPath, zim.Container, ["clone", "dispose"], "zimContainer", false);
//-54.5

/*--
zim.Button = function(width, height, label, backgroundColor, rollBackgroundColor, color, rollColor, borderColor, borderRollColor, borderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss, dashed, backing, rollBacking, rollPersist, icon, rollIcon, toggle, toggleBacking, rollToggleBacking, toggleIcon, rollToggleIcon, toggleEvent, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, waitBacking, rollWaitBacking, waitIcon, rollWaitIcon, align, valign, indent, indentHorizontal, indentVertical, style, group, inherit)

Button
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Makes a button with rollover and many more features - see parameters.
You will need to pass in a Label to change the font properties of the button from the default.
You will then need to add the button to the stage and add a mousedown or click event.
Button rollover is done automatically.

You can set a backing display object (Shape, Bitmap, Pattern, etc.) in place of the standard rectangle.
You can set an icon display object in place of the standard text
You can set the Button to toggle between text, backings or icons
SEE the ZIM Pizzazz series for a growing selection of backings, patterns and icons
https://zimjs.com/bits/view/pizzazz.html
https://zimjs.com/bits/view/icons.html
https://zimjs.com/patterns.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var button = new Button("CLICK");
button.center(stage);
button.on("click", function(){zog("clicking");});

// OR add custom label (needed to change label color for instance)
var label = new Label({
	text:"POWER OPTION",
	size:40,
	backgroundColor:"violet",
	fontOptions:"bold"
});
var button = new Button({
	label:label,
	width:390,
	height:110,
	backgroundColor:"purple",
	rollBackgroundColor:"MediumOrchid",
	borderWidth:8,
	borderColor:"violet",
	gradient:.3,
	corner:0
});
button.center(stage);
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 200) the width of the button
height - (default 60) the height of the button
label - (default "CLICK") ZIM Label or plain text with default settings (white)
backgroundColor - (default "orange") background color of button (any CSS color)
rollBackgroundColor - (default "lightorange") rollover background color of button
color - (default "white") label color of button (any CSS color) unless a custom Label is set
rollColor - (default "white") rollover label color of button
borderColor - (default null) the color of the border
borderRollColor - (default borderColor) the rollover color of the border
borderWidth - (default null) thickness of the border
corner - (default 20) the round of the corner (set to 0 for no corner)
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
shadowColor - (default rgba(0,0,0,.3)) set to -1 for no shadow
shadowBlur - (default 14) how blurred the shadow is if the shadow is set
hitPadding - (default 0) adds extra hit area to the button (good for mobile)
	Note that if the button alpha is 0 the button will still be active if hitPadding is not equal to 0
	Set the hitPadding property to 0 in this case - thanks Frank Los for the notice
gradient - (default 0) 0 to 1 (try .3) adds a gradient to the button
gloss - (default 0) 0 to 1 (try .1) adds a gloss to the button
dashed - (default false) set to true to turn the border to dashed - if the borderColor or borderWidth is provided
backing - (default null) a Display object for the backing of the button (eg. Shape, Bitmap, Container, Sprite)
	assumed to be center registration for easy positioning *** as with all backings
	see ZIM Pizzazz module for a fun set of Button Shapes like Boomerangs, Ovals, Lightning Bolts, etc.
	https://zimjs.com/bits/view/pizzazz.html
rollBacking - (default null) a Display object for the backing of the rolled-on button
rollPersist - (default false) set to true to keep rollover state when button is pressed even if rolling off
icon - (default null) set to display object to add icon at the center of the button and remove label
	assumed to be center registration for easy positioning *** as with all icons
	https://zimjs.com/bits/view/icons.html
rollIcon - (default null) set to display object to show icon on rollover
toggle - (default null) set to string to toggle with label
	the button will not toggle if there is a wait parameter set
	see the backings and icons parameters below for custom toggle settings
	there are no color settings for toggle - to do this, just make a second button and swap buttons
toggleBacking - (default null) set to display object to set a different backing for toggled state
rollToggleBacking - (default null) set to display object to set a backing for the rolled toggle state
toggleIcon - (default null) set to display object to add icon at the center of the button and remove label in toggle state
rollToggleIcon - (default null) set to display object to show icon on rollover in toggle state
toggleEvent - (default mousedown for mobile and click for not mobile) what event causes the toggle
wait - (default null) - String word for button to show when button is pressed and a wait state is desired
	LOADING: this can be used as a loading message - so change the button to "LOADING"
	When the asset has loaded, use the clearWait() method to return to the normal button or toggled button state
	CONFIRMING: this can also be used to confirm user action rather than a full new confirm panel
	Set wait:"CONFIRM", set the waitBackgroundColor and rollWaitBackground parameters to red and the waitTime parameter to 4000
	In a button mousedown (must use mousedown - not click or tap if ACTIONEVENT is mousedown - the default),
	check if the waiting property is true to test for confirmation
	The waiting property will not be true for the first button press but will be true during the wait period
	Perhaps set the waitModal parameter to true to clearWait() if the user clicks off the button
	Use the clearWait() method to clear or cancel the wait status - for instance, if the pane the button is in is closed
waitTime - (default 30000 - 30 seconds) milliseconds (ms) to show wait state before reverting to normal button
waitBackgroundColor - (default red) color to make button during wait time if wait is set
rollWaitBackgroundColor - (default rollColor) color for button when waiting and rolled over
waitColor - (default label's color) color to make text during wait time if wait is set
rollWaitColor - (default label's roll color) color for text when waiting and rolled over
waitModal - (default false) set to true to clearWait() if the user clicks off the button
waitEnabled - (default true) set to false to disable button while wait is in progress
waitBacking - (default null) set to display object to set a different backing for wait state
rollWaitBacking - (default null) set to display object to a different roll backing for wait state
waitIcon - (default null) set to display object to add icon at the center of the button and remove label in wait state
rollWaitIcon - (default null) set to display object to show icon on rollover in wait state
align - (default "center") horizontal align of the label
valign - (default "center") vertical align of the label
indent - (default 10) indent of label when align or valign is set
indentHorizontal - (default indent) horizontal indent of label when align or valign is set
indentVertical - (default indent) vertical indent of label when align or valign is set
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
setBacking(type, newBacking) - dynamically set any type of backing for button (if null removes backing for that type)
	Backing types are: "backing", "rollBacking", "toggleBacking", "rollToggleBacking", "waitBacking", "rollWaitBacking"
	note - all backing will have a pattern property if a pattern is set as a backing
setIcon(type, newIcon) - dynamically set any type of icon for button (if null removes icon for that type)
	Icon types are: "icon", "rollIcon", "toggleIcon", "rollToggleIcon", "waitIcon", "rollWaitIcon"
toggle(state) - forces a toggle of label, backing and icon if set
	state defaults to null so just toggles if left blank
	pass in true to go to the toggled state and false to go to the original state
	returns object for chaining
wait() - forces a wait state - with wait text, backings and icons if set
clearWait() - clears a wait state of the button - sets it back to normal
removeWait() - removes (and clears) a wait state of the button so it will not trigger again
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
text - references the text property of the Label object of the button
label - gives access to the label
color - get or set non-rolled on label color (if no icon specified)
rollColor - get or set rolled on label color
backgroundColor - get or set non-rolled on backing color (if no backing specified)
rollBackgroundColor - get or set rolled on backing color
rollPersist - default is false - set to true to keep rollover state when button is pressed even if rolling off
borderColor - get or set non-rolled on border color
borderRollColor - get or set the border rolled color
hitPadding - extra padding for interactivity - see hitPadding parameter for extra notes
backing - references the backing of the button
	use setBacking() to change as with all backings
	note - all backings will have a pattern property if a pattern is set as a backing
rollBacking - references the rollBacking (if set)
icon - references the icon of the button (if set)
	use setIcon() to change as with all icons
rollIcon - references the rollIcon (if set)
rolled - read-only true if button is being rolled over else false
toggled - read-only true if button is in toggled state, false if button is in original state
toggleBacking - references the toggle backing (if set)
rollToggleBacking - references the toggle roll backing (if set)
toggleIcon - references the toggle icon (if set)
rollToggleIcon - references the toggle roll icon (if set)
waiting - read-only true if button is in the waiting state within wait time
waitBacking - references the wait backing (if set)
rollWaitBacking - references the wait roll backing (if set)
waitIcon - references the wait icon (if set)
rollWaitIcon - references the wait roll icon (if set)
focus - get or set the focus property of the Button used for tabOrder
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties
for example seeing toggle take effect

EVENTS
dispatches a "waited" event if button is in wait state and the wait time has completed
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+55
	zim.Button = function(width, height, label, backgroundColor, rollBackgroundColor, color, rollColor, borderColor, borderRollColor, borderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss, dashed, backing, rollBacking, rollPersist, icon, rollIcon, toggle, toggleBacking, rollToggleBacking, toggleIcon, rollToggleIcon, toggleEvent, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, waitBacking, rollWaitBacking, waitIcon, rollWaitIcon, align, valign, indent, indentHorizontal, indentVertical, style, group, inherit) {
		var sig = "width, height, label, backgroundColor, rollBackgroundColor, color, rollColor, borderColor, borderRollColor, borderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss, dashed, backing, rollBacking, rollPersist, icon, rollIcon, toggle, toggleBacking, rollToggleBacking, toggleIcon, rollToggleIcon, toggleEvent, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, waitBacking, rollWaitBacking, waitIcon, rollWaitIcon, align, valign, indent, indentHorizontal, indentVertical, style, group, inherit";
		var duo; if (duo = zob(zim.Button, arguments, sig, this)) return duo;
		z_d("55");
		this.group = group;
		var DS = style===false?{}:zim.getStyle("Button", group, inherit);

		if (zot(width)) width=DS.width!=null?DS.width:200;
		if (zot(height)) height=DS.height!=null?DS.height:60;
		this.zimContainer_constructor(width, height, null, null, false);
		this.type = "Button";

		if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"#C60";
		if (zot(rollBackgroundColor)) rollBackgroundColor=DS.rollBackgroundColor!=null?DS.rollBackgroundColor:"#F93";
		if (zot(color)) color=DS.color!=null?DS.color:"white";
		if (zot(rollColor)) rollColor=DS.rollColor!=null?DS.rollColor:"white";
		if (zot(waitBackgroundColor)) waitBackgroundColor=DS.waitBackgroundColor!=null?DS.waitBackgroundColor:backgroundColor;
		if (zot(rollWaitBackgroundColor)) rollWaitBackgroundColor=DS.rollWaitBackgroundColor!=null?DS.rollWaitBackgroundColor:rollBackgroundColor;
		var originalBorderColor = borderColor;
		var originalBorderWidth = borderWidth;
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:null;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(borderRollColor)) borderRollColor=DS.borderRollColor!=null?DS.borderRollColor:borderColor;
		if (zot(corner)) corner=DS.corner!=null?DS.corner:20;
		if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:14;
		if (zot(hitPadding)) hitPadding=DS.hitPadding!=null?DS.hitPadding:0;
		if (zot(align)) align=DS.align!=null?DS.align:"center";
		if (zot(valign)) valign=DS.valign!=null?DS.valign:"center";
		if (zot(indent)) indent=DS.indent!=null?DS.indent:10;
		if (zot(indentHorizontal)) indentHorizontal=DS.indentHorizontal!=null?DS.indentHorizontal:indent;
		if (zot(indentVertical)) indentVertical=DS.indentVertical!=null?DS.indentVertical:indent;
		if (zot(gradient)) gradient = DS.gradient!=null?DS.gradient:0;
		if (zot(gloss)) gloss = DS.gloss!=null?DS.gloss:0;
		if (zot(label)) {if (zot(icon)) {label = DS.label!=null?DS.label:"PRESS";} else {label = "";}}
		var toggleOkay = (!zot(toggle) || !zot(toggleBacking) || !zot(rollToggleBacking) || !zot(toggleIcon) || !zot(rollToggleIcon)) && zot(wait) && zot(waitBacking) && zot(rollWaitBacking);
		if (toggleOkay && zot(toggleEvent)) toggleEvent = zim.mobile()?"mousedown":"click";
		// text, size, font, color, rollColor, shadowColor, shadowBlur, align, valign
		if (typeof label === "string" || typeof label === "number") label = new zim.Label({
			text:label, size:DS.size!=null?DS.size:36, font:DS.font!=null?DS.font:"arial", color:DS.color!=null?DS.color:color, rollColor:DS.rollColor!=null?DS.rollColor:rollColor, align:align, valign:valign, rollPersist:DS.rollPersist!=null?DS.rollPersist:false,
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore", shiftVertical:DS.shiftVertical!=null?DS.shiftVertical:0, shiftHorizontal:DS.shiftHorizontal!=null?DS.shiftHorizontal:0,
			style:false, group:this.group
		});
		if (zot(rollPersist)) rollPersist = DS.rollPersist!=null?DS.rollPersist:false;
		this.rollPersist = rollPersist;
		if (zot(dashed)) dashed = DS.dashed!=null?DS.dashed:false;
		if (!zot(toggle) && toggle.type=="Label") {if (zon) zog("ZIM Button() - do not pass Label to toggle parameter - just pass a String")}

		var that = this;
		this.mouseChildren = false;
		this.cursor = "pointer";
		that.focus = false;
		that.rolled = false;
		var stage;

		//~~~~~~~~~~~~~  BACKINGS
		// also see manual setting of backings beneath getter setter methods
		if (zot(backing)) backing = DS.backing!=null?DS.backing.clone():null;
		if (zot(backing)) that.backing = new zim.Rectangle(width,height,backgroundColor,null,null,corner,dashed,null,false).centerReg(null, null, false);
		else that.backing = backing; // if backing is null - we have no custom backing - this test is used later

		that.rollBacking = zot(rollBacking)?(DS.rollBacking!=null?DS.rollBacking.clone():null):rollBacking;
		that.waitBacking = zot(waitBacking)?(DS.waitBacking!=null?DS.waitBacking.clone():null):waitBacking;
		that.rollWaitBacking = zot(rollWaitBacking)?(DS.rollWaitBacking!=null?DS.rollWaitBacking.clone():null):rollWaitBacking;
		that.toggleBacking = zot(toggleBacking)?(DS.toggleBacking!=null?DS.toggleBacking.clone():null):toggleBacking;
		that.rollToggleBacking = zot(rollToggleBacking)?(DS.rollToggleBacking!=null?DS.rollToggleBacking.clone():null):rollToggleBacking;

		var backingTypes = ["backing", "rollBacking", "toggleBacking", "rollToggleBacking", "waitBacking", "rollWaitBacking"];
		var t;
		var b;
		for (var i=0; i<backingTypes.length; i++) {
			t = backingTypes[i];
			b = that[t]; // access to object passed to parameter or null
			if (b) {
				if (b.type == "Pattern") {
					b = setPattern(t, b);
				} else if (shadowColor != -1 && shadowBlur > 0) {
					b.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
				}
				// assumes center reg
    			b.x = width / 2;
    			b.y = height / 2;
			}
		}
		that.addChild(that.backing);
		if (borderWidth) {
			that.border = new zim.Rectangle(width, height, "rgba(0,0,0,0)", borderColor, borderWidth, corner, null, null, false);
			that.addChild(that.border);
		}
		function setPattern(type, pattern) {
			that[type] = new zim.Container(width, height, null, null, false).centerReg(null, null, false);
			if (shadowColor != -1 && shadowBlur > 0) {
				var shadowRect = new zim.Rectangle(width-2, height-2, "#666", null, null, corner, null, null, false).center(that[type]);
				shadowRect.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
			}
			var mask = that[type].mask = new zim.Rectangle(width, height, type.indexOf("roll")>=0?rollBackgroundColor:backgroundColor, null, null, corner, null, null, false).addTo(that[type]);
			pattern.centerReg(mask);
			pattern.setMask(mask.shape);
			that[type].pattern = pattern;
			return that[type];
		}

		//~~~~~~~~~~~~~  ICONS
		that.icon = zot(icon)?(DS.icon!=null?DS.icon.clone():null):icon;
		that.rollIcon = zot(rollIcon)?(DS.rollIcon!=null?DS.rollIcon.clone():null):rollIcon;
		that.waitIcon = zot(waitIcon)?(DS.waitIcon!=null?DS.waitIcon.clone():null):waitIcon;
		that.rollWaitIcon = zot(rollWaitIcon)?(DS.rollWaitIcon!=null?DS.rollWaitIcon.clone():null):rollWaitIcon;
		that.toggleIcon = zot(toggleIcon)?(DS.toggleIcon!=null?DS.toggleIcon.clone():null):toggleIcon;
		that.rollToggleIcon = zot(rollToggleIcon)?(DS.rollToggleIcon!=null?DS.rollToggleIcon.clone():null):rollToggleIcon;
		var iconTypes = ["icon", "rollIcon", "toggleIcon", "rollToggleIcon", "waitIcon", "rollWaitIcon"];
		for (var i=0; i<iconTypes.length; i++) {
			var ty = iconTypes[i];
			var ic = that[ty]; // access to object passed to parameter or null
			if (ic) {
				// assumes center reg
    			ic.x = width / 2;
    			ic.y = height / 2;
			}
		}
		if (that.icon) that.addChild(that.icon);

		//~~~~~~~~~~~~~  GRADIENT AND GLOSS

		if (!Array.isArray(corner)) corner = [corner, corner, corner, corner];
		if (gradient > 0) { // add an overlay
			var gr = new createjs.Shape();
			gr.graphics.lf(["rgba(255,255,255,"+gradient+")","rgba(0,0,0,"+gradient+")"], [0, 1], 0, 0, 0, height-borderWidth);
			gr.graphics.rc(borderWidth/2, borderWidth/2, width-borderWidth, height-borderWidth, corner[0], corner[1], corner[2], corner[3]);
			this.addChild(gr);
		}
		if (gloss > 0) { // add an overlay
			var gl = new createjs.Shape();
			gl.graphics.f("rgba(255,255,255,"+gloss+")");
			gl.graphics.rc(borderWidth/2, borderWidth/2, width-borderWidth, (height-borderWidth)/2, corner[0], corner[1], 0, 0);
			gl.graphics.f("rgba(0,0,0,"+gloss+")");
			gl.graphics.rc(borderWidth/2, height/2, width-borderWidth, (height-borderWidth)/2, 0, 0, corner[2], corner[3]);
			this.addChild(gl);
		}

		//~~~~~~~~~~~~~  HITAREA AND LABEL

		var hitArea;
		var rect;
		if (hitPadding > 0) makeHitArea();
		function makeHitArea() {
			rect = new createjs.Shape();
			rect.graphics.f("#000").r(-hitPadding,-hitPadding,width+hitPadding*2,height+hitPadding*2);
			that.hitArea = hitArea = rect;
		}

		this.addChild(label);
		label.center(this);
		label.y+=(typeof zimDefaultFrame!="undefined" && zimDefaultFrame.retina!=true)?1:0;
		this.label = label;

		zim.pos(label, (align=="left"||align=="right")?indentHorizontal:null, (valign=="top"||valign=="bottom")?indentVertical:null, align=="right", valign=="bottom");


		//~~~~~~~~~~~~~  TOGGLE STATE
		this.toggled = false;
		if (toggleOkay) {
			var originalText = label.text;
			this.on(toggleEvent, function() {
				that.toggled = !that.toggled;
				setToggled();
			});
		}

		function setToggled() {
			if (that.toggled) {
				if (!zot(toggle)) that.text = toggle;
				// for toggle - may start in rollover or could be manually called
				if (that.rolled) {
					if (that.rollToggleBacking) changeObject("rollToggleBacking", that.rollToggleBacking);
					else if (that.toggleBacking) changeObject("toggleBacking", that.toggleBacking);
					if (that.rollToggleIcon) changeObject("rollToggleIcon", that.rollToggleIcon);
					else if (that.toggleIcon) changeObject("toggleIcon", that.toggleIcon);
				} else {
					if (that.toggleBacking) changeObject("toggleBacking", that.toggleBacking);
					if (that.toggleIcon) changeObject("toggleIcon", that.toggleIcon);
				}
			} else {
				that.text = originalText;
				setOriginalObjects();
			}
			if (that.stage) that.stage.update();
		}

		function setOriginalObjects() {
			if (that.rolled) {
				if (zot(backing) && !that.rollBacking) that.backing.color = rollBackgroundColor;
				if (that.rollBacking) changeObject("rollBacking", that.rollBacking);
				else if (that.backing) changeObject("backing", that.backing);
				if (that.rollIcon) changeObject("rollIcon", that.rollIcon);
				else if (that.icon) changeObject("icon", that.icon);
				else changeObject("icon", null);
			} else {
				if (zot(backing)) that.backing.color = backgroundColor;
				if (that.backing) changeObject("backing", that.backing);
				if (that.icon) changeObject("icon", that.icon);
				else changeObject("icon", null);
			}
		}

		this.toggle = function(state) {
			if (!toggleOkay) {
				if (zon) zog("zim.Button() - can't toggle with wait parameters provided");
				return that;
			}
			if (zot(state)) {
				that.toggled = !that.toggled;
			} else {
				that.toggled = state;
			}
			setToggled();
			return that;
		}

		//~~~~~~~~~~~~~  WAIT STATE
		var pressCheck = false;
		that.waiting = false;
		var willBeWaiting = false;
		var waitTimeout;
		var waitStartText;
		// var waitStartBackgroundColor;
		var waitStartTextBackgroundColor = that.label.color;
		var waitStartRollTextBackgroundColor = that.label.rollColor;
		var waitStartEnabled;
		var waitModalEvent;
		var waitEvent = this.on("mousedown", function() {
			pressCheck=true;
			doWait();
		});

		function doWait() {
			if ((!zot(wait) || !zot(waitBacking) || !zot(rollWaitBacking)) && !that.waiting) {
				willBeWaiting = true;
				if (zot(waitEnabled)) waitEnabled = true;
				if (waitModal) waitModalEvent = that.stage.on("stagemousedown", function(e) {
					if (!that.hitTestPoint(e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY)) that.clearWait();
				}, null, true); // run only once
				// wait before setting the waiting property so first click is not a waiting
				setTimeout(function(){that.waiting = true;}, 50);
				// set button to waiting state
				waitStartText = label.text;
				if (!zot(waitColor)) that.label.color = waitColor;
				if (!zot(rollWaitColor)) that.label.rollColor = rollWaitColor;
				waitStartEnabled = that.enabled;
				if (!waitEnabled && that.enabled) that.enabled = false;
				if (!zot(wait)) that.text = wait;

				if (that.rolled) {
					if (zot(backing) && !that.rollWaitBacking) that.backing.color = rollWaitBackgroundColor;
					if (that.rollWaitBacking) changeObject("rollWaitBacking", that.rollWaitBacking);
					else if (that.waitBacking) changeObject("waitBacking", that.waitBacking);
					if (that.rollWaitIcon) changeObject("rollWaitIcon", that.rollWaitIcon);
					else if (that.waitIcon) changeObject("waitIcon", that.waitIcon);
				} else {
					if (zot(backing) && !that.waitBacking) that.backing.color = waitBackgroundColor;
					if (that.waitBacking) changeObject("waitBacking", that.waitBacking);
					if (that.waitIcon) changeObject("waitIcon", that.waitIcon);
				}

				if (zot(waitTime)) waitTime = 30000; // 30 seconds
				if (waitTimeout) waitTimeout.clear();
				waitTimeout = timeout(waitTime, function() {
					// set button to proper text, icon, backing, colors, etc.
					if (!that.enabled) that.enabled = waitStartEnabled;
					that.clearWait();
					that.dispatchEvent("waited");
				});
				if (that.stage) that.stage.update();
			}
		};
		this.wait = function() {
			doWait();
		}
		this.clearWait = function() {
			if (!waitTimeout) return that;
			if (waitModalEvent) that.stage.off("stagemousedown", waitModalEvent);
			waitTimeout.clear();
			that.text = waitStartText;
			setOriginalObjects();
			that.label.color = waitStartTextBackgroundColor;
			that.label.rollColor = waitStartRollTextBackgroundColor;
			setTimeout(function(){that.waiting = false;}, 55); // give time for first click to see not waiting yet
			willBeWaiting = false;
			if (that.stage) that.stage.update();
			return that;
		}
		this.removeWait = function() {
			that.clearWait();
			wait = null;
			that.waitBacking = null;
			that.rollWaitBacking = null;
			that.off("mousedown", waitEvent);
			return that;
		}

		//~~~~~~~~~~~~~  INTERACTION

		this.on("pressup", function(){
			pressCheck=false;
			if (that.rollPersist && !reallyOn) removeRoll();
		});

		// visually swap button backing or icon
		// on clicks if wait or toggle and on mouseover and mouseout
		// note - icon will be removed if newObject is null
		// BUT - backing will be ignored if newObject is null
		// so these act slightly differently!
		function changeObject(type, newObject) {
			if (type.indexOf("con")>=0) { // icon
				for (var i=0; i<iconTypes.length; i++) {
					var ty = iconTypes[i];
					var ic = that[ty];
					that.removeChild(ic);
				}
				if (that[type]) that.addChildAt(that[type], 1);
			} else {
				if (!that[type]) return;
				for (var i=0; i<backingTypes.length; i++) {
					var t = backingTypes[i];
					var b = that[t];
					that.removeChild(b);
				}
				if (that[type]) that.addChildAt(that[type], 0);
			}
		}

		var reallyOn = false;
		this.on("mouseover", buttonOn);
		function buttonOn(e) {
			that.rolled = true;
			reallyOn = true;

			// specific to each setting
			// so can have a rollover backing even without a backing
			// also... if no rollWaitBacking or rollToggleBacking
			// then if there is the backing for these, still set the backing
			// all backings get removed and current backing object is placed
			// normal buttons (with no backings) get borders on rectangle
			// backings get overlayed border with borderColor and borderRollColor
			// will have to track each state normal, toggle and wait
			// do not set colors on any custom backings (aside from border colors)

			if (willBeWaiting) {
				if (zot(backing) && zot(that.rollWaitBacking)) that.backing.color = zot(rollWaitBackgroundColor)?rollBackgroundColor:rollWaitBackgroundColor;
				changeObject("rollWaitBacking", that.rollWaitBacking);
				if (that.rollWaitIcon) changeObject("rollWaitIcon", that.rollWaitIcon);
			} else if (toggleOkay && that.toggled) {
				if (zot(backing) && zot(that.rollToggleBacking)) that.backing.color = rollBackgroundColor;
				changeObject("rollToggleBacking", that.rollToggleBacking);
				if (that.rollToggleIcon) changeObject("rollToggleIcon", that.rollToggleIcon);
			} else {
				if (zot(backing)) that.backing.color = rollBackgroundColor;
				else if (!zot(backing.mask)) that.backing.mask.color = rollBackgroundColor;
				changeObject("rollBacking", that.rollBacking);
				if (that.rollIcon) changeObject("rollIcon", that.rollIcon);
			}
			if (that.border) that.border.borderColor = borderRollColor;

			if (that.label.showRollColor) that.label.showRollColor();
			if (that.stage) that.stage.update();
		}

		this.on("mouseout", buttonOff); // thanks Maxime Riehl
		function buttonOff(e) {
			reallyOn = false;
			that.off("mouseout", buttonOff); // not working and not needed? 2018
			if (that.rollPersist) {
				if (!pressCheck) removeRoll();
			} else {
				removeRoll();
			}
		}
		function removeRoll() {
			that.rolled = false;
			if (willBeWaiting || that.waiting) {
				if (zot(backing) && zot(that.waitBacking)) that.backing.color = zot(waitBackgroundColor)?backgroundColor:waitBackgroundColor;
				if (that.waitBacking) changeObject("waitBacking", that.waitBacking);
				else changeObject("backing", that.backing);
				if (that.waitIcon) changeObject("waitIcon", that.waitIcon);
				else if (that.icon) changeObject("icon", that.icon);
				else changeObject("icon", null);
			} else if (that.toggled && toggleOkay) {
				if (zot(backing) && zot(that.toggleBacking)) that.backing.color = backgroundColor;
				if (that.toggleBacking) changeObject("toggleBacking", that.toggleBacking);
				else changeObject("backing", that.backing);
				if (that.toggleIcon) changeObject("toggleIcon", that.toggleIcon);
				else if (that.icon) changeObject("icon", that.icon);
				else changeObject("icon", null);
			} else {
				if (zot(backing))  that.backing.color = backgroundColor;
				else if (!zot(backing.mask)) that.backing.mask.color = backgroundColor;
				changeObject("backing", that.backing);
				if (that.icon) changeObject("icon", that.icon);
				else changeObject("icon", null);
			}

			if (that.border) that.border.borderColor = borderColor;

			if (that.label.showRollColor) that.label.showRollColor(false);
			if (that.stage) that.stage.update();
		}


		Object.defineProperty(that, 'text', {
			get: function() {
				var t = (label.text == " ") ? "" : label.text;
				return t;
			},
			set: function(value) {
				label.text = value;
				label.center(this);
				label.y+=1;
			}
		});

		Object.defineProperty(that, 'color', {
			get: function() {
				return color;
			},
			set: function(value) {
				color = value;
				if (that.label && !zot(that.label.color)) {
					that.label.color = color;
				}
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		Object.defineProperty(that, 'rollColor', {
			get: function() {
				return rollColor;
			},
			set: function(value) {
				rollColor = value;
				if (that.label) {
					that.label.rollColor = rollColor;
				}
			}
		});

		Object.defineProperty(that, 'backgroundColor', {
			get: function() {
				return backgroundColor;
			},
			set: function(value) {
				backgroundColor = value;
				if (that.backing.color) {
					that.backing.color = backgroundColor;
				} else if (that.backing.mask) {
					that.backing.mask.color = backgroundColor;
				}
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		Object.defineProperty(that, 'rollBackgroundColor', {
			get: function() {
				return rollBackgroundColor;
			},
			set: function(value) {
				rollBackgroundColor = value;
				if (that.rollBacking && that.rollBacking.color) {
					that.rollBacking.color = rollBackgroundColor;
				} else if (that.rollBacking && that.rollBacking.mask) {
					that.rollBacking.mask.color = rollBackgroundColor;
				}
			}
		});

		Object.defineProperty(that, 'borderColor', {
			get: function() {
				return borderColor;
			},
			set: function(value) {
				borderColor = value;
				if (!that.rolled) {
					if (that.backing && that.backing.borderColor) that.backing.borderColor = value;
					if (that.border) that.border.borderColor = value;
				}
			}
		});

		Object.defineProperty(that, 'borderRollColor', {
			get: function() {
				return borderRollColor;
			},
			set: function(value) {
				borderRollColor = value;
				if (that.rolled) {
					if (that.backing && that.backing.borderColor) that.backing.borderColor = value;
					if (that.border) that.border.borderColor = value;
				}
			}
		});

		Object.defineProperty(that, 'hitPadding', {
			get: function() {
				return hitPadding;
			},
			set: function(value) {
				hitPadding = value;
				if (hitPadding == 0) {
					if (hitArea) {
						this.hitArea = null;
					}
				} else {
					makeHitArea();
				}
			}
		});

		this._enabled = true;
		this.startMouseChildren = this.mouseChildren;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				if (that._enabled) that.startMouseChildren = that.mouseChildren;
				if (value) {
					zenable(that, value);
					that.mouseChildren = that.startMouseChildren;
				} else {
					removeRoll();
					zenable(that, value);
				}
				label.color = label.color;
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		// setBacking or leave backing parameter blank to remove this type of backing
		this.setBacking = function(type, newBacking) {
			setObject(type, newBacking);
		}
		// setIcon or leave icon parameter blank to remove this type of icon
		this.setIcon = function(type, newIcon) {
			setObject(type, newIcon);
		}
		function setObject(type, newObject) {
			if (zot(type)) return that;
			if (that.contains(that[type])) {
				that.removeChild(that[type]);
				if (newObject) that.addChildAt(newObject, type.indexOf("con")>=0?that.numChildren-1:0);
				if (that.stage) that.stage.update();
			}
			if (newObject) {
				if (zot(backing) && type == "backing") backing = newObject;
				if (newObject.type == "Pattern") newObject = setPattern(type, newObject);
				that[type] = newObject;
				that[type].x = width/2;
				that[type].y = height/2;
			} else {
				that[type] = null;
			}
			return that;
		}
		if (style!==false) if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			var but = new zim.Button(
				width, height, label.clone(), backgroundColor, rollBackgroundColor, color, rollColor, originalBorderColor, borderRollColor, originalBorderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss, dashed,
				!zot(backing)?that.backing.clone():null,
				!zot(rollBacking)?that.rollBacking.clone():null,
				rollPersist,
				!zot(icon)?icon.clone():null, !zot(rollIcon)?rollIcon.clone():null,
				toggle,
				!zot(toggleBacking)?toggleBacking.clone():null,
				!zot(rollToggleBacking)?rollToggleBacking.clone():null,
				!zot(toggleIcon)?toggleIcon.clone():null,
				!zot(rollToggleIcon)?rollToggleIcon.clone():null,
				toggleEvent,
				wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled,
				!zot(waitBacking)?waitBacking.clone():null,
				!zot(rollWaitBacking)?rollWaitBacking.clone():null,
				!zot(waitIcon)?waitIcon.clone():null,
				!zot(rollWaitIcon)?rollWaitIcon.clone():null,
				align, valign, indent, indentHorizontal, indentVertical,
				style,
				this.group
			);
			return that.cloneProps(but);
		}
	}
	zim.extend(zim.Button, zim.Container, "clone", "zimContainer", false);
	//-55

/*--
zim.CheckBox = function(size, label, startChecked, color, backgroundColor, borderColor, borderWidth, corner, margin, indicatorType, indicatorColor, tap, style, group, inherit)

CheckBox
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A checkbox that when pressed toggles the check and a checked property.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var checkBox = new CheckBox(50, "TEST");
checkBox.center(stage);
checkBox.on("change", function() {
	zog(checkBox.checked); // will be true then false, etc.
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
size - (default 60) size in pixels (always square)
label - (default null) ZIM Label object - or String to make a default label (black)
startChecked - (default false) an initial parameter to set checked if true
color - (default "#111") the text color of the label
backgroundColor - (default "rgba(255,255,255,.8)") the background color of the box
borderColor - (default "#111") the color of the border
borderWidth - (default size/10) thickness of the border
corner - (default 0) the round of the corner
   can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
margin - (default 10) is on outside of box so clicking or pressing is easier
indicatorType - (default check) could be square (box) or x
indicatorColor - (default borderColor or black if no border) the color of the indicator
tap - (default false) set to true to tap to activate CheckBox rather than mousedown or click
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
setChecked(Boolean) - defaults to true to set button checked (or use checked property)
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
checked - gets or sets the check of the box
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
label - gives access to the label
text - the text of the label
background - the Rectangle of the checkBox
indicator - gives access to the check mark ie. indicator.sca(.8);
indicatorColor - get or set the color of the indicator
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)

EVENTS
dispatches a "change" event when pressed on but not when the checked property is set

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+56
	zim.CheckBox = function(size, label, startChecked, color, backgroundColor, borderColor, borderWidth, corner, margin, indicatorType, indicatorColor, tap, style, group, inherit) {
		var sig = "size, label, startChecked, color, backgroundColor, borderColor, borderWidth, corner, margin, indicatorType, indicatorColor, tap, style, group, inherit";
		var duo; if (duo = zob(zim.CheckBox, arguments, sig, this)) return duo;
		z_d("56");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "CheckBox";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(size)) size = DS.size!=null?DS.size:60;
		if (zot(label)) label = DS.label!=null?DS.label:null;
		if (zot(startChecked)) startChecked = DS.startChecked!=null?DS.startChecked:false;
		var myChecked = startChecked;
		if (zot(color)) color = DS.color!=null?DS.color:"#111";
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"rgba(255,255,255,.8)";
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:"#111";
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:size/10;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = size/10;
		if (zot(corner)) corner=DS.corner!=null?DS.corner:0;
		if (typeof label === "string" || typeof label === "number") label = new zim.Label({
			text:label, size:size*5/6, color:color, valign:"center",
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
			group:this.group
		});
		if (zot(margin)) margin = DS.margin!=null?DS.margin:size/5;
		if (indicatorType != "box" && indicatorType != "square" && indicatorType != "x") indicatorType = DS.indicatorType!=null?DS.indicatorType:"check";
		if (zot(indicatorColor)) indicatorColor = DS.indicatorColor!=null?DS.indicatorColor:borderWidth>0?borderColor:"black";
		this.setBounds(-margin, -margin, size+margin*2, size+margin*2);
		if (zot(tap)) tap = DS.tap!=null?DS.tap:false;


		var that = this;
		this.cursor = "pointer";

		var box = new zim.Rectangle(size, size, backgroundColor, null, null, corner);
		var box2 = new zim.Rectangle(size*5/7, size*5/7,"rgba(0,0,0,0)", borderColor, borderWidth, corner);
		box2.x = size/7; box2.y = size/7;
		this.addChild(box, box2);

		var fullWidth = size;

		if (label) {
			label.center(that);
			label.x = that.getBounds().width;
			this.label = label;
			this.setBounds(-margin, -margin, size+margin*3+label.getBounds().width, Math.max(size+margin*2, label.getBounds().height));
			fullWidth = label.x + label.width;
		}

		var backing = new zim.Shape({style:false});
		g = backing.graphics;
		g.f("rgba(0,0,0,.01)").r(
			this.getBounds().x,
			this.getBounds().y,
			fullWidth+(margin*2),
			this.getBounds().height
		);
		this.hitArea = backing;
		// hitArea will stop rollovers on labels but oh well

		var check = new zim.Shape({style:false});
		var g2 = check.graphics;
		if (indicatorType == "check") {
			g2.f(indicatorColor).p("AnQAdICBiaIEEDZIF8nfICfB4In/KPg"); // width about 90 reg in middle
		} else if (indicatorType == "box" || indicatorType == "square") {
			g2.f(indicatorColor).dr(-35,-35,70,70);
		} else { // x
			g2.f(indicatorColor).p("AmJEVIEUkTIkXkWIB4h5IEWEYIETkTIB4B3IkTESIEQERIh4B4IkRkRIkSEVg"); // width about 90 reg in middle
		}

		var cW = 95
		check.setBounds(-cW/2, -cW/2, cW, cW);
		var scale = size/(cW+66);

		check.scaleX = check.scaleY = scale;
		check.alpha = .9;
		check.x = size/2;
		check.y = size/2;

		if (myChecked) this.addChild(check);

		if (tap) {
			this.tap(toggleCheck);
		} else {
			this.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", toggleCheck);
		}

		Object.defineProperty(that, 'checked', {
			get: function() {
				return myChecked;
			},
			set: function(value) {
				that.setChecked(value);
			}
		});

		this.toggle = function(type) {
			if (zot(type)) type = !myChecked;
			that.setChecked(type);
		}

		Object.defineProperty(that, 'toggled', {
			get: function() {
				return myChecked;
			},
			set: function(value) {
				that.setChecked(value);
			}
		});

		Object.defineProperty(that, 'text', {
			get: function() {
				if (label) return label.text;
			},
			set: function(value) {
				if (label) {
					label.text = value;
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				};
			}
		});

		Object.defineProperty(check, 'indicatorColor', {
			get: function() {
				return indicatorColor;
			},
			set: function(value) {
				if (myChecked) {that.removeChild(check);}
				check = new createjs.Shape();
				g2 = check.graphics;
				indicatorColor = value;
				g2.f(indicatorColor).p("AnQAdICBiaIEEDZIF8nfICfB4In/KPg");
				check.scaleX = check.scaleY = scale;
				check.alpha = .9;
				check.x = size/2;
				check.y = size/2;
				if (myChecked) that.addChild(check);
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		Object.defineProperty(that, 'indicator', {
			get: function() {
				return check;
			},
			set: function(value) {
				zog("ZIM CheckBox - check is read only");
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
			}
		});

		function toggleCheck(e) {
			myChecked = !myChecked;
			that.setChecked(myChecked);
			that.dispatchEvent("change");
		}

		this.setChecked = function(value) {
			if (zot(value)) value = true;
			myChecked = value;
			if (myChecked) {
				that.addChild(check);
			} else {
				that.removeChild(check);
			}
			if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			return that;
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.CheckBox(size, label?label.clone():"", startChecked, color, backgroundColor, borderColor, borderWidth, corner, margin, indicatorType, indicatorColor, tap, style, this.group, inherit));
		}
	}
	zim.extend(zim.CheckBox, zim.Container, "clone", "zimContainer", false);
	//-56

/*--
zim.RadioButtons = function(size, buttons, vertical, color, backgroundColor, spacing, margin, always, indicatorColor, style, group, inherit)

RadioButtons
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A radio button set that lets you pick from choices.
Radio buttons can display radio buttons vertically (default) or horizontally.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var radioButtons = new RadioButtons(50, ["ONE", "TWO", "THREE"]);
radioButtons.center(stage);
radioButtons.on("change", function() {
	zog(radioButtons.text); // will be ONE, TWO or THREE
	zog(radioButtons.selectedIndex); // will be 0, 1, or 2
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
size - (default 60) in pixels
buttons - an array of button data objects as follows:
	[{label:ZIM Label or text, id:optional id, selected:optional Boolean}, {etc...}]
	or just a list of labels for default labels ["hi", "bye", "what!"]
vertical - (default true) displays radio buttons vertically - set to false to display horizontally
color - (default "#111") the text color of the label
backgroundColor - (default "rgba(255,255,255,.8)") the background color of the circle
borderColor - (default "#111") the color of the border
borderWidth - (default size/9) thickness of the border
spacing - (size*.2 for vertical and size for horizontal) the space between radio button objects
margin - (size/5) the space around the radio button itself
always - (default false) if set true, cannot click on selection to unselect it
indicatorColor - (default borderColor or black) the color of the indicator
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
setSelected(num) - sets the selected index (or use selectedIndex) -1 is default (none)
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
selected - gets the selected object - selected.label, selected.id, etc.
selectedIndex - gets or sets the selected index of the buttons
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
label - current selected label object
text - current selected label text
id - current selected id
buttons - an array of button Container objects holding the shape and label (note - different than buttons parameter)
labels - an array of the ZIM Label objects. labels[0].text = "YUM"; labels[2].y -= 10;
indicators - an array of the zim Shape dot objects. indicators[0].color = "yellow";
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties
and stage.update() in change event to see component change its graphics

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)

EVENTS
dispatches a "change" event when pressed but not when selectedIndex is set
then ask for the properties above for info

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+57
	zim.RadioButtons = function(size, buttons, vertical, color, backgroundColor, borderColor, borderWidth, spacing, margin, always, indicatorColor, style, group, inherit) {
		var sig = "size, buttons, vertical, color, backgroundColor, borderColor, borderWidth, spacing, margin, always, indicatorColor, style, group, inherit";
		var duo; if (duo = zob(zim.RadioButtons, arguments, sig, this)) return duo;
		z_d("57");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "RadioButtons";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(size)) size = DS.size!=null?DS.size:60;
		size = Math.max(5, size);
		if (zot(buttons)) buttons = DS.buttons!=null?DS.buttons:["A", "B", "C"];
		if (zot(vertical)) vertical = DS.vertical!=null?DS.vertical:true;
		if (zot(color)) color = DS.color!=null?DS.color:"#111";
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"rgba(255,255,255,.8)";
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:"#111";
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:size/9;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = size/10;
		if (zot(spacing)) spacing = (vertical) ? DS.spacing!=null?DS.spacing:size*.2 : DS.spacing!=null?DS.spacing:size;
		if (zot(margin)) margin =  DS.margin!=null?DS.margin:size/5;
		if (zot(indicatorColor)) indicatorColor =  DS.indicatorColor!=null?DS.indicatorColor:borderWidth>0?borderColor:"black";;

		var that = this;
		this.cursor = "pointer";
		this.labels = [];
		this.indicators = [];
		var currentObject; // reference to the current data object
		if (typeof buttons == "string") {
			// convert to buttons object literal (for cloning)
			var bString = buttons;
			buttons = [];
			for (var i=0; i<bString.length; i++) {
				buttons.push({label:bString[i]});
			}
		}

		var buttonContainer = new zim.Container({style:false});
		this.addChild(buttonContainer);

		function pressBut(e) {
			var index = buttonContainer.getChildIndex(e.target);
			if (always) {if (that.selectedIndex == index) return;}
			that.setSelected(index);
			that.dispatchEvent("change");
		}

		// loop through data and call makeButton() each time
		makeButtons();

		var currentK;
		for (var k=0; k<buttonContainer.numChildren; k++) {
			currentK = buttonContainer.getChildAt(k);
			currentK.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", pressBut);
		}

		var lastBut;
		function makeButtons() {
			// test for duplicate selected true properties (leave last selected)
			var data; var selectedCheck = false;
			for (var i=buttons.length-1; i>=0; i--) {
				data = buttons[i];
				if (data.selected && data.selected === true) {
					if (!selectedCheck) {
						selectedCheck = true; // first item marked selected
						that.id = data.id;
					} else {
						data.selected = "false"; // turn off selected
					}
				}
			}
			buttonContainer.removeAllChildren();
			that.buttons = [];
			var but; var currentLocation = 0;
			for (var i=0; i<buttons.length; i++) {
				data = buttons[i];

				if (typeof data === "string" || typeof data === "number") {
					var d = {selected:false, label:new zim.Label({
						text:data, size:size*5/6, color:DS.color!=null?DS.color:color, valign:"center",
						backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
						group:that.group
					})};
					data = d;
				}
				if (data.label && typeof data.label === "string" || typeof data.label === "number") {
					data.label = new zim.Label({text:data.label, size:DS.size!=null?DS.size:size*5/6, color:DS.color!=null?DS.color:color, valign:"center"});
				}
				that.labels.push(data.label);
				data.index = i;
				buttons[i] = data; // for cloning
				but = makeButton(data.selected, data.label);
				but.type = "RadioButton"; // singular
				but.obj = data;
				if (data.selected) currentObject = but.obj;

				buttonContainer.addChild(but);

				if (vertical) {
					but.y = currentLocation;
					currentLocation += but.getBounds().height + spacing;
				} else {
					but.x = currentLocation;
					currentLocation += but.getBounds().width + spacing;
				}
			}
		}

		// making a single button - similar to CheckBox class
		function makeButton(mySelected, label) {
			var but = new zim.Container({style:false});
			that.buttons.push(but);
			but.mouseChildren = false;
			but.setBounds(-margin, -margin, size+margin*2, size+margin*2);

			var box = new zim.Shape({style:false});
			var g = box.graphics;
			g.f(backgroundColor).dc(size/2,size/2,size/1.85);
			g.s(borderColor).ss(borderWidth).dc(size/2, size/2, size/2-size/2/5);
			but.addChild(box);

			var check = but.check = new zim.Circle(size/5.2, indicatorColor, null, null, null, null, null, false);
			that.indicators.push(check);
			check.mouseEnabled = false;
			check.alpha = .95;
			check.regX = check.regY = -size/2;

			var fullWidth = size;

			if (label) {
				but.addChild(label);
				label.x = but.getBounds().width;
				label.y = size/2;
				but.setBounds(-margin, -margin, size+margin*2+label.getBounds().width, Math.max(size+margin*2, label.getBounds().height));
				fullWidth = label.x + label.width;
				but.text = label.text;
			}
			if (mySelected) {
				but.addChild(check);
				that.label = label;
				if (that.label) that.text = label.text;
			}

			var backing = new zim.Shape({style:false});
			g = backing.graphics;
			g.f("rgba(0,0,0,.01)").r(
				but.getBounds().x,
				but.getBounds().y,
				fullWidth+(margin*2),
				but.getBounds().height
			);
			but.hitArea = backing;
			// hitArea will stop rollovers on labels but oh well

			return(but);
		}
		if (!this.getBounds()) this.setBounds(0,0,size,size);
		this.setBounds(-margin,-margin,this.getBounds().width+margin*2,this.getBounds().height);

		// the main function that sets a button selected (after the initial makeButton)
		// this gets called by the setter methods below and the click event up top
		this.setSelected = function(value) {
			if (zot(value)) value = -1;
			if (value != -1 && !buttonContainer.getChildAt(value)) return;
			var but;
			for (var i=0; i<buttonContainer.numChildren; i++) {
				but = buttonContainer.getChildAt(i);
				but.removeChild(but.check);
			}
			if (value >= 0) {
				but = buttonContainer.getChildAt(value);
				var lastIndex = -2;
				if (currentObject) lastIndex = currentObject.index;
				currentObject = but.obj;
			}
			if (value == -1 || lastIndex == currentObject.index) {
				currentObject = null;
				that.id = null;
				that.label = null;
				that.text = "";
			} else {
				but.addChild(but.check);
				that.id = currentObject.id;
				that.label = currentObject.label;
				if (that.label) that.text = that.label.text;
			}
			if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			return that;
		}

		// getter setter methods

		Object.defineProperty(that, 'selected', {
			get: function() {
				return currentObject;
			},
			set: function(value) {
				zog("ZIM RadioButton - selected is read only");
			}
		});

		Object.defineProperty(that, 'selectedIndex', {
			get: function() {
				return (currentObject) ? currentObject.index : -1;
			},
			set: function(value) {
				var index = value;
				if (always) {if (that.selectedIndex == index) return;}
				that.setSelected(index);
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
			}
		});

		zimStyleTransforms(this, DS)
		this.clone = function() {
			var buttonsCopy = zim.copy(buttons);
			for (var i=0; i<buttonsCopy.length; i++) {
				buttonsCopy[i].label = buttonsCopy[i].label.clone();
			}
			return that.cloneProps(new zim.RadioButtons(size, buttonsCopy, vertical, color, backgroundColor, borderColor, borderWidth, spacing, margin, always, indicatorColor, style, this.group, inherit));
		}
	}
	zim.extend(zim.RadioButtons, zim.Container, "clone", "zimContainer", false);
	//-57

/*--
zim.Toggle = function(width, height, label, startToggled, backgroundColor, margin, indicatorType, indicatorColor, tap, toggleBackgroundColor, color, borderColor, borderWidth, corner, indicatorCorner, shadowColor, shadowBlur, time, labelLeft, style, group, inherit)

Toggle
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A Toggle button that toggles off and on - with optional labels
Thanks Andi Erni for the initial design and coding of the Toggle.
See: https://zimjs.com/explore/toggle.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
new Toggle({label:"ON"}).center().change(function (e) {
	zog(e.target.toggled)
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 80) the width of the toggle (less labels)
height - (default 50) the height of the toggle
label - (default null) an optional ZIM Label (or text for default label properties)
	also see labelLeft for left side text
startToggled - (default false) set to true to start in the toggled position
backgroundColor - (default "#C60") dark orange - set to any HTML color for background color
margin - (default 10) the distance around and between the toggle and its parts
indicatorType - (default "circle" or "round") set to "rectangle" or "square" for square indicator
indicatorColor - (default "#111")
toggleBackgroundColor - (default "#F93") orange - for toggled background color
	try setting the borderColor to the same color as the background for inner color change effect
color - (default "#111") the font color of the toggle
borderColor - (default null) the color of the border
borderWidth - (default null - or 1 if borderColor) the size of the border
corner - (default half the height) a corner radius - or an array of corners [topLeft, topRight, bottomRight, bottomLeft]
indicatorCorner - (default 0) change the corner radius of a rectangle toggleType - or an array of corners [topLeft, topRight, bottomRight, bottomLeft]
shadowColor - (default "rgba(0,0,0,.3)" if shadowBlur) the shadow color - set to -1 for no shadow
shadowBlur - (default 14 if shadowColor) the shadow blur - set to -1 for no shadow
time - (default 200) time in milliseconds (ms) for toggle to animate
labelLeft - (default null) an optional ZIM Label for the left side of the toggle (or text for default label properties)
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
toggle(state) - toggle the toggle - state defaults to true - set to false to un-toggle
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
toggled - gets the toggled state of the toggle - same as selected
text - gets the selected label text or "on" / "off" if no label
indicator - access to the indicator object
background - access to background Rectangle
label - access to the label if provided
labelLeft - access to the label on the left if provided
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "change" event when pressed but not when toggle() is used

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+57.5
	zim.Toggle = function(width, height, label, startToggled, backgroundColor, margin, indicatorType, indicatorColor, tap, toggleBackgroundColor, color, borderColor, borderWidth, corner, indicatorCorner, shadowColor, shadowBlur, time, labelLeft, style, group, inherit) {
        var sig = "width, height, label, startToggled, backgroundColor, margin, indicatorType, indicatorColor, tap, toggleBackgroundColor, color, borderColor, borderWidth, corner, indicatorCorner, shadowColor, shadowBlur, time, labelLeft, style, group, inherit";
        var duo; if (duo = zob(zim.Toggle, arguments, sig, this)) return duo;
		z_d("57.5");

		this.group = group;
		var DS = style===false?{}:zim.getStyle("Toggle", this.group, inherit);
		if (zot(width)) width=DS.width!=null?DS.width:80;
        if (zot(height)) height=DS.height!=null?DS.height:50;
        this.zimContainer_constructor(width, height, null, null, false);
        this.type = "Toggle";

        if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"#C60";
        if (zot(margin)) margin = DS.margin!=null?DS.margin:10; //20;
        if (zot(indicatorType)) indicatorType=DS.indicatorType!=null?DS.indicatorType:"circle";
        if (zot(indicatorColor)) indicatorColor=DS.indicatorColor!=null?DS.indicatorColor:"#fff";
        if (zot(toggleBackgroundColor)) toggleBackgroundColor=DS.toggleBackgroundColor!=null?DS.toggleBackgroundColor:"#F93";
        if (zot(color)) color=DS.color!=null?DS.color:"#111";
        if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:null;
        if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
        if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
        else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(corner)) corner=DS.corner!=null?DS.corner:indicatorType!="circle"?0:25;
        if (zot(indicatorCorner)) indicatorCorner=DS.indicatorCorner!=null?DS.indicatorCorner:0;
        if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
        if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:14;
        if (zot(startToggled)) startToggled=DS.startToggled!=null?DS.startToggled:false;
        if (zot(time)) time=DS.time!=null?DS.time:100;

		var that = this;
        that.cursor = "pointer";

        if (typeof label === "string" || typeof label === "number") label = this.label = new zim.Label({
			text:label, size:height*5/6, color:color, valign:"center",
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
			group:this.group
		});

        if (typeof labelLeft === "string" || typeof labelLeft === "number") labelLeft = this.labelLeft = new zim.Label({
			text:labelLeft, size:height*5/6, color:color, valign:"center",
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
			group:this.group
		});

        this.background = new zim.Rectangle(width, height, backgroundColor, borderColor, borderWidth, corner).addTo(this);
        if (indicatorType=="rectangle" || indicatorType=="square") this.indicator = new zim.Rectangle(height*.65, height*.65, indicatorColor, null, null, indicatorCorner).center(this.background).pos(height*.2, null, startToggled);
        else this.indicator = new zim.Circle(height*.35, indicatorColor).center(this.background).pos(height*.175, null, startToggled);
        this.toggled = startToggled;
        that.background.color = that.toggled?toggleBackgroundColor:backgroundColor;

        if (shadowColor != -1 && shadowBlur > 0) {
            this.background.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
        }

        if (label) {
			this.addChild(label);
			label.x = width + 2 + margin + borderWidth;
			label.y = height/2;
			this.label = label;
			this.setBounds(-margin, -margin, width+margin*3+borderWidth+label.getBounds().width, Math.max(height+margin*2, label.getBounds().height));
		}

        if (labelLeft) {
			this.addChild(labelLeft);
			labelLeft.x = 0;
            that.background.x += labelLeft.width + 3 + margin + borderWidth;
            that.label.x += labelLeft.width + 3 + margin + borderWidth;
			labelLeft.y = height/2;
			this.labelLeft = labelLeft;
			this.setBounds(-margin, -margin, that.getBounds().width+labelLeft.width + 3 + margin + borderWidth, that.getBounds().height);
		}
		this.expand(zim.mobile()?20:10);

		this.tap(function (e) {
			if (labelLeft) {
				var point = that.localToGlobal(labelLeft.width+3+margin+borderWidth+width/2, 0);
				if ((e.stageX/that.stage.scaleX < (point.x-width/2) && !that.toggled) || (e.stageX/that.stage.scaleX >= (point.x+width/2) && that.toggled)) return;
			}
			that.toggled = !that.toggled;
	        setToggle();
			that.dispatchEvent("change");
		}, zim.mobile()?20:10);

		var swipe = new zim.Swipe(this);
		swipe.on("swipe", function (e) {
			if (e.swipeX==0) {
				return;
			} else if (e.swipeX==1 && that.toggled) {
				return;
			} else if (e.swipeX==-1 && !that.toggled) {
				return;
			}
			that.toggled = !that.toggled;
            setToggle();
			that.dispatchEvent("change");
		});

        function setToggle(immediate){
            var oldX = that.indicator.x;
			var t = time;
			if (immediate===true) t = 0;
            if (indicatorType=="rectangle" || indicatorType=="square") {
                that.indicator.pos(height*.2, null, that.toggled);
				if (time>0) that.indicator.animate({props:{x:oldX}, from:true, time:t});
            } else {
                that.indicator.pos(height*.175, null, that.toggled);
				if (time>0) that.indicator.animate({props:{x:oldX}, from:true, time:t});
			}
            that.background.color = that.toggled?toggleBackgroundColor:backgroundColor;

			that.text = that.toggled?(that.label?that.label.text:"on"):(that.labelLeft?that.labelLeft.text:"off");

			if (that.zimAccessibility) {
				var string = "Toggle set to " + (that.toggled?(that.label?that.label.text+".":"on."):(that.labelLeft?that.labelLeft.text+".":"off."));
				setTimeout(function() {that.zimAccessibility.talk(string);}, 50);
			}
        }

		Object.defineProperty(that, 'textLeft', {
			get: function() {
				if (labelLeft) return labelLeft.text;
			},
			set: function(value) {
				if (labelLeft) {
					labelLeft.text = value;
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				};
			}
		});


        that.toggle = function(state, immediate) {
            var lastToggle = that.toggled;
            if (zot(state)) state = true;
            that.toggled = state;
            if (lastToggle != that.toggled) setToggle(immediate);
            return that;
        }
		that.text = that.toggled?(that.label?that.label.text:"on"):(that.labelLeft?that.labelLeft.text:"off");

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
			}
		});

        if (style!==false) zimStyleTransforms(this, DS);;
		this.clone = function() {
        	return that.cloneProps(new zim.Toggle(width, height, label?label.clone():"", startToggled, backgroundColor, margin, indicatorType, indicatorColor, tap, toggleBackgroundColor, color, borderColor, borderWidth, corner, indicatorCorner, shadowColor, shadowBlur, time, labelLeft?labelLeft.clone():"", style, this.group, inherit));
		}
    }
	zim.extend(zim.Toggle, zim.Container, "clone", "zimContainer", false);
	//-57.5

/*--
zim.Tip = function(text, align, valign, margin, marginH, marginV, outside, target, size, font, color, rollColor, shadowColor, shadowBlur, textAlign, textValign, lineWidth, lineHeight, fontOptions, backing, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, group, inherit)

Tip
zim class - extends a a zim.Label which extends a zim.Container which extends a createjs.Container

DESCRIPTION
A Tip() can be used to show some extra information - the tip disapears after an amount of time
Tip has easy positioning along the inside edges or the outside edges of a target.

NOTE: Tip places the tip on the stage when the show() method is called
You can reposition with .mov() etc. if desired

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
new Tip("Press Circle").show(1000); // wait one second and show tip at 40 pixels from bottom right

var circle = new Circle().center().tap(function () {
	circleTip.show();
});
var circleTip = new Tip({
	text:"This is a default ZIM Circle",
	backgroundColor:white,
	color:black,
	outside:true, // outside the circle
	target:circle,
	align:"center",
	valign:"bottom",
	margin:14,
	corner:0,
	size:20
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
text - (default "Here's a tip!") String for the the text of the tip
align - (default "right") the horizontal position of the tip ("left", "center"/"middle", "right")
valign - (default "bottom") the vertical position of the tip ("top", "center"/"middle", "bottom")
margin - (default 40) distance from side (unless centered) in pixels
marginH - (default margin) distance from horizontal edges
marginV - (default margin) distance from vertical edges
outside - (default false) set to true to place Tip on outside of container
target - (default ZimDefaultFrame's stage) tip is placed on stage relative to container
** the rest are parameters for a Label (align and valign are set as textAlign and textValign)
size - (default 36) the size of the font in pixels
font - (default arial) the font or list of fonts for the text
color - (default "black") color of font (any CSS color)
rollColor - (default color) the rollover color of the font
shadowColor - (default "rgba(0,0,0,.3)") for no shadow - set to any css color to see
shadowBlur - (default 1) if shadow is present
textAlign - ((default "left") text registration point alignment also "center" and "right"
textValign - (default "center") vertical registration point alignment alse "middle / center", "bottom"
lineWidth - (default false) for no wrapping (use \n) Can set to number for wrap
lineHeight - (default getMeasuredLineHeight) set to number to adjust line height
fontOptions - (default null) css VALUES as a single string for font-style font-variant font-weight
   eg. "italic bold small-caps" or just "italic", etc.
backing - (default null) a Display object for the backing of the label (eg. Shape, Bitmap, Container, Sprite)
   see ZIM Pizzazz module for a fun set of Shapes like Boomerangs, Ovals, Lightning Bolts, etc.
outlineColor - (default null - or black if outlineWidth set) - the color of the outline of the text
outlineWidth - (default null - or (size*.2) if outlineColor set) - the thickness of the outline of the text
backgroundColor - (default null) set to CSS color to add a rectangular color around the label
   The background color will change size to match the text of the label
   Note: the backgroundColor is different than a backing which can be any Display Object
   and background parameters are ignored if a backing parameter is set
backgroundBorderColor - (default null) the background stroke color
backgroundBorderWidth - (default null) thickness of the background border
corner - (default 0) the round of corner of the background if there is one
   can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
backgroundDashed - (default null) set to true for dashed background border (if backgroundBorderColor or backgroundBorderWidth set)
padding - (default 10 if backgroundColor set) places the border this amount from text (see paddingHorizontal and paddingVertical)
   padding parameters are ignored if there is no backgroundColor set (also ignored if a backing parameter is set)
paddingHorizontal - (default padding) places border out at top bottom
paddingVertical - (default padding) places border out at left and right
shiftHorizontal - (default 0) move the label (CreateJS Text) inside the Label container horizontally
shiftVertical - (default 0) move the label (CreateJS Text) inside the Label container vertically
rollPersist - (default false) set to true to maintain rollover stage as long as mousedown or press is activated (used by Buttons)
labelWidth - (default null) the same as the lineWidth - the text will wrap at the labelWidth (added to match labelHeight)
labelHeight - (default null) the height of the text - setting this will probably alter the font size - so the size parameter is overwritten
   for labelHeight to work, the labelWidth must also be set
   using labelWidth and labelHeight together allow you to fit as much text into specified width and height dimensions
maxSize - (default null) set to limit the font size when using labelWidth and labelHeight
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
show(delay, time) - show the tip - delay in ms defaults to 0 and time in ms defaults to 2000
hide() - hides tip - show() will also hide the tip automatically after the time provided
clear() - hides tip and removes the call to a delayed tip using a delay time in show()
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: See all methods of a Label() such as setColorRange(), etc.
ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
align - get or set the horizontal alignment
valign - get or set the vertical alignment
text - get or set the text of the Tip
ALSO: See all properties of a Label() such as size, color, etc.
ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+57.6
	zim.Tip = function(text, align, valign, margin, marginH, marginV, outside, target, size, font, color, rollColor, shadowColor, shadowBlur, textAlign, textValign, lineWidth, lineHeight, fontOptions, backing, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, group, inherit) {
        var sig = "text, align, valign, margin, marginH, marginV, outside, target, size, font, color, rollColor, shadowColor, shadowBlur, textAlign, textValign, lineWidth, lineHeight, fontOptions, backing, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, group, inherit";
        var duo; if (duo = zob(zim.Tip, arguments, sig, this)) return duo;
		z_d("57.6");

		this.group = group;
		var DS = style===false?{}:zim.getStyle("Tip", this.group, inherit);

		if (zot(text)) text = DS.text!=null?DS.text:"Here's a tip!";
		if (zot(margin)) margin = DS.margin!=null?DS.margin:40;
		if (zot(marginH)) marginH = DS.marginH!=null?DS.marginH:margin;
		if (zot(marginV)) marginV = DS.marginV!=null?DS.marginV:margin;
		if (zot(align)) align = DS.align!=null?DS.align:"right";
		if (zot(valign)) valign = DS.valign!=null?DS.valign:"bottom";
		if (zot(outside)) outside = DS.outside!=null?DS.outside:false;
        if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:blue;
        if (zot(color)) color = DS.color!=null?DS.color:white;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:25;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:25;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:25;
		if (zot(paddingHorizontal)) paddingHorizontal = DS.paddingHorizontal!=null?DS.paddingHorizontal:14+(Array.isArray(corner)?corner[0]:corner);
		if (zot(shadowColor) || shadowColor=="ignore") shadowColor=(DS.shadowColor!=null&&shadowColor!="ignore")?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur) || shadowBlur=="ignore") shadowBlur=(DS.shadowBlur!=null&&shadowBlur!="ignore")?DS.shadowBlur:1;

	    this.zimLabel_constructor(text, size, font, color, rollColor, null, null, textAlign, textValign, lineWidth, lineHeight, fontOptions, backing, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, group, DS);
        this.type = "Tip";

		if (outside) {
			marginH = -marginH;
			marginV = -marginV;
		}
        var that = this;

		that.align = align;
		that.valign = valign;

        this.background.sha(shadowColor, 3, 5, shadowBlur);
        this.show = function(delay, time) {
			if (zot(delay)) delay = DS.delay!=null?DS.delay:0;
			if (zot(time)) time = DS.time!=null?DS.time:2000;
			var mess = "zim Tip(): Please pass in a reference to a container with bounds set as parameter to Tip";
			if (zot(target)) {
				if (zimDefaultFrame) {
					target = zimDefaultFrame.stage;
				} else {
					zog(mess);
					return that;
				}
			} else if (!target.getBounds) {
				zog(mess);
				return that;
			} else if (zot(target.stage)) {
				zog("zim display - Waiter(): The container must have a stage property");
				return that;
			}

			if (delay > 0) {
				that.showID = zim.timeout(delay, doShow);
			} else {
				doShow();
			}

			function doShow() {

				if (target.boundsToGlobal) var b = target.boundsToGlobal();
				else var b = target.getBounds();
				var container = new Container(b.x, b.y, b.width, b.height);
				container.zimTemp = true;
				container.loc(0, 0, target.stage);

				if (that.align=="center" || that.align=="middle" || that.valign=="center" || that.valign=="middle") {
					that.center(container);
				}
	            that.pos((that.align=="center" || that.align=="middle")?null:marginH, (that.valign=="center" || that.valign=="middle")?null:marginV, (that.align=="right"), (that.valign=="bottom"), container);
				if (outside) {
					if (that.align=="right") that.x += that.width;
					else if (that.align=="left") that.x -= that.width;
					if (that.valign=="bottom") that.y += that.height;
					else if (that.valign=="top") that.y -= that.height;
				}

				that.addTo(container.stage); // will transfer over position...
				if (container.zimTemp && container.removeFrom) {container.removeFrom(); container = null;}
				container = that.stage;

				if (that.timeoutID) that.timeoutID.clear();
	            that.timeoutID = zim.timeout(time, function () {
	                that.hide();
	                container.stage.update();
	            });
	            if (that.upID) container.stage.off("stagemouseup", that.upID);
	            that.upID = container.stage.on("stagemouseup", function () {
					that.hide();
	                container.stage.update();
	            })
				container.stage.update();
			}
			return that;
        }
        this.hide = function() {
            this.removeFrom();
            if (this.timeoutID) this.timeoutID.clear();
            if (this.upID && target.stage) target.stage.off("stagemouseup", this.downID);
			return that;
        }
		this.clear = function() {
			if (that.showID) that.showID.clear();
			that.hide();
		}

        if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
        	return that.cloneProps(new zim.Tip(text, align, valign, margin, marginH, marginV, outside, target, size, font, color, rollColor, shadowColor, shadowBlur, textAlign, textValign, lineWidth, lineHeight, fontOptions, backing, outlineColor, outlineWidth, backgroundColor, backgroundBorderColor, backgroundBorderWidth, corner, backgroundDashed, padding, paddingHorizontal, paddingVertical, shiftHorizontal, shiftVertical, rollPersist, labelWidth, labelHeight, maxSize, style, this.group, inherit));
		}
    }
	zim.extend(zim.Tip, zim.Label, "clone", "zimLabel", false);
	//-57.6

/*--
zim.Panel = function(width, height, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, backgroundColor, borderColor, borderWidth, corner, arrow, align, shadowColor, shadowBlur, draggable, boundary, extraButton, style, group, inherit)

Panel
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A simple panel with titleBar and optional arrow for more panels.
Panel can be set draggable but does not have a close button - try a Pane() or Window() for that
See: https://zimjs.com/explore/panel.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var panel = new Panel({titleBar:series("TODAY", "TOMORROW")})
	.center();

// make a couple pages for the panels

// content for panel 1
var today = new Container(panel.width, panel.height).addTo(panel);
var sun = new Circle(30, yellow).center(today);

// content for panel 2
var tomorrow = new Container(panel.width, panel.height); // do not add yet
var label = new Label("-30").center(tomorrow);

// event to change content as panels change
panel.on("change", function () {
	if (today.parent) {
		today.removeFrom();
		tomorrow.center(panel);
	} else {
		tomorrow.removeFrom();
		today.center(panel);
	}
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 250) the width of the panel
height - (default 300) the height of the panel
titleBar - |ZIM VEE| (default "PANEL") a String or ZIM Label title for the panel that will be presented on a titleBar across the top
titleBarColor - |ZIM VEE| (default "black") the text color of the titleBar if a titleBar is requested
titleBarBackgroundColor - |ZIM VEE| (default "rgba(0,0,0,.2)") the background color of the titleBar if a titleBar is requested
titleBarHeight - (default fit label) the height of the titleBar if a titleBar is requested
backgroundColor - |ZIM VEE| (default #eee) background color (use clear - or "rbga(0,0,0,0)" for no background)
borderColor - |ZIM VEE| (default #888) border color
borderWidth - (default 1) the thickness of the border
corner - (default 0) the round of corner
   can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
arrow - (default true if more than one panel) set to false to not show an arrow if multiple panels
align - (default "left") set to "center", "middle" or "right" to align the label on the titleBar
shadowColor - (default "rgba(0,0,0,.3)" if shadowBlur) the shadow color - set to -1 for no shadow
shadowBlur - (default 14 if shadowColor) the shadow blur - set to -1 for no shadow
draggable - (default true if titleBar) set to false to not allow dragging titleBar to drag window
boundary - (default null) set to ZIM Boundary() object - or CreateJS.rectangle()
extraButton - (default null) creates a little square button with the letter R for reset
	this is made with the group style id of "extraButton"
	use the extraButton property to access the button to change its label or capture an event, etc.
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
nextPanel(index, event) - show next panel - the panels are set up to be a series or random or function based
	this means there is not necessarily an order to be able to go backwards to... so, only forward!
	If a series is provided to the Panel title, etc. then the index can be used to go to the title in the series at the index
	event (default false) will dispatch a change event if nextPanel is called
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
titleBar - access to the titleBar container
label - access to the label of the current panel
text - access to the text of the current panel
arrow - access to the next arrow
background - access to the background Rectangle
extraButton - access to the Label for the extra button if extraButton parameter is set to true
	use this to set the text in the button (a one letter button is expected - for instance, i for info, x for close, etc.)
overlay - access to the overlay Rectangle used if enabled = false
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "change" event when arrow is pressed to go to the next panel

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+57.7
	zim.Panel = function(width, height, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, backgroundColor, borderColor, borderWidth, corner, arrow, align, shadowColor, shadowBlur, draggable, boundary, extraButton, style, group, inherit) {
        var sig = "width, height, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, backgroundColor, borderColor, borderWidth, corner, arrow, align, shadowColor, shadowBlur, draggable, boundary, extraButton, style, group, inherit";
        var duo; if (duo = zob(zim.Panel, arguments, sig, this)) return duo;
		z_d("57.7");

		this.group = group;
		var DS = style===false?{}:zim.getStyle("Panel", this.group, inherit);
		if (zot(width)) width=DS.width!=null?DS.width:250;
        if (zot(height)) height=DS.height!=null?DS.height:300;
        this.zimContainer_constructor(width, height, null, null, false);
        this.type = "Panel";

		if (zot(titleBar)) titleBar = DS.titleBar!=null?DS.titleBar:"PANEL";
		if (zot(titleBarColor)) titleBarColor = DS.titleBarColor!=null?DS.titleBarColor:"#fff";
		if (zot(titleBarBackgroundColor)) titleBarBackgroundColor = DS.titleBarBackgroundColor!=null?DS.titleBarBackgroundColor:"#555";
		if (zot(titleBarHeight)) titleBarHeight = DS.titleBarHeight!=null?DS.titleBarHeight:30;

        if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"#eee";
        if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:"#888";
        if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
        if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
        else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(corner)) corner=DS.corner!=null?DS.corner:5;
		if (zot(align)) align=DS.align!=null?DS.align:"left";
		if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:14;
		if (zot(draggable)) draggable = DS.draggable!=null?DS.draggable:false;
		if (zot(boundary)) boundary = DS.boundary!=null?DS.boundary:null;
		if (zot(arrow)) arrow=DS.arrow!=null?DS.arrow:zim.vee(titleBar);
		if (!Array.isArray(corner)) corner = [corner,corner,corner,corner];

		var that = this;
		var background = this.background = new zim.Rectangle(width, height, backgroundColor, borderColor, borderWidth, corner).addTo(this);
		if (shadowColor != -1 && shadowBlur > 0) {
			this.background.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
		}
		var titleBarValue = titleBar; // as we assign the container to titleBar later
		var t = zim.Pick.choose(titleBarValue);
		var tBarColor = zim.Pick.choose(titleBarColor);
		var tBarBackgroundColor = zim.Pick.choose(titleBarBackgroundColor);
		var pBackgroundColor = zim.Pick.choose(backgroundColor);
		var pBorderColor = zim.Pick.choose(borderColor);

		if (typeof t == "string") t = new zim.Label({
			text:t, color:tBarColor, size:DS.size!=null?DS.size:20,
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
			group:this.group
		});
		var titleBarLabel = that.titleBarLabel = t;
		if (zot(tBarBackgroundColor)) tBarBackgroundColor = "rgba(0,0,0,.2)";
		that.titleBar = titleBar = new zim.Container(width, titleBarHeight, null, null, false).loc(0,0,that);
		var titleBarRect = that.titleBar.backing = new zim.Rectangle(width+borderWidth, titleBarHeight, tBarBackgroundColor, null, null, [corner[0]*.95, corner[1]*.95, 0, 0], true, null, false).center(titleBar);
		positionBar();
		that.label = t;
		that.text = t.text;

		this.nextPanel = function(index, event) {
			var t = zot(index)||zot(titleBarValue.array)?zim.Pick.choose(titleBarValue):titleBarValue.array[index];
			var tBarColor = zot(index)||zot(titleBarColor.array)?zim.Pick.choose(titleBarColor):titleBarColor.array[index];
			var tBarBackgroundColor = zot(index)||zot(titleBarBackgroundColor.array)?zim.Pick.choose(titleBarBackgroundColor):titleBarBackgroundColor.array[index];
			var pBackgroundColor = zot(index)||zot(backgroundColor.array)?zim.Pick.choose(backgroundColor):backgroundColor.array[index];
			var pBorderColor = zot(index)||zot(borderColor.array)?zim.Pick.choose(borderColor):borderColor.array[index];
			if (typeof t == "string") t = new zim.Label({
				text:t, color:tBarColor, size:DS.size!=null?DS.size:20,
				backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
				group:this.group
			});
			that.label = t;
			that.text = t.text;
			titleBarLabel.removeFrom();
			titleBarLabel = that.titleBarLabel = t;
			positionBar();
			titleBarRect.color = tBarBackgroundColor;
			that.background.color = pBackgroundColor;
			that.background.borderColor = pBorderColor;
			if (event) that.dispatchEvent("change");
			if (!OPTIMIZE && that.stage) that.stage.update();
		}

		function positionBar() {
			if (align=="right") titleBarLabel.center(titleBar).pos(Math.max(corner[0]/2, 10), null, true);
			else if (align=="center" || align=="middle") titleBarLabel.center(titleBar);
			else titleBarLabel.center(titleBar).loc(Math.max(corner[0]/2, 10));
		}

		if (draggable) {
			titleBar.cursor = "pointer";
			titleBar.on("mousedown", function() {
				that.drag({rect:boundary, currentTarget:true});
			});
			titleBar.on("pressup", function() {
				that.noDrag();
			});
		}

		if (arrow > 0) {
			var next = that.arrow = new zim.Shape(-20,-20,40,40,null,false);
			next.graphics.f(titleBarColor).p("AiJieIETCeIkTCfg"); // width about 90 reg in middle
			next.centerReg(titleBar).scaleTo(titleBar, null, 70).alp(.8).hov(1).expand();
			if (align=="right") next.pos(Math.max(corner[1]/2, 10));
			else next.pos(Math.max(corner[1]/2, 10), null, true);
			next.cursor = "pointer";
			next.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", function(){
				that.nextPanel();
				that.dispatchEvent("change");
			});
		}

		if (!zot(extraButton)) {
			var extraButton = that.extraButton = new zim.Button({
				label:"R",
				width:50,
				height:50,
				group:"PanelExtra"
			}).scaleTo(titleBar, null, 70).centerReg(titleBar).expand();
			if (align=="left") {
				extraButton.pos(arrow>0?next.x-next.width-15:Math.max(corner[1]/2, 10) );
			} else {
				extraButton.pos((arrow>0&&align!="center")?next.x+next.width: Math.max(corner[1]/2, 10));
			}
		}

		var overlay = that.overlay = new zim.Rectangle(width, height).alp(.7);

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
				if (!value) that.overlay.addTo(that);
				else that.overlay.removeFrom();
				if (!OPTIMIZE && that.stage) that.stage.update();
			}
		});

        if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
        	return that.cloneProps(new zim.Toggle(width, height, titleBar?titleBar.clone():"", titleBarColor, titleBarBackgroundColor, titleBarHeight, backgroundColor, borderColor, borderWidth, corner, arrow, align, shadowColor, shadowBlur, draggable, boundary, extraButton, style, this.group, inherit));
		}
    }
	zim.extend(zim.Panel, zim.Container, "clone", "zimContainer", false);
	//-57.7

/*--
zim.Pane = function(width, height, label, backgroundColor, color, draggable, resets, modal, corner, backdropColor, shadowColor, shadowBlur, center, displayClose, backdropClose, backing, fadeTime, container, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, close, closeColor, style, group, inherit)

Pane
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Adds a window for alerts, etc.
You need to call the pane.show() to show the pane and pane.hide() to hide it.
You do not need to add it to the stage - it adds itself centered.
You can change the x and y (the origin and registration point are in the middle).

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var pane = new Pane(300, 200, "Watch out!", "#CCC");
pane.show(); // pressing anywhere will close pane (see parameters for options)
END EXAMPLE

EXAMPLE
var pane = new Pane({width:600, height:250, modal:false, displayClose:false});
var cancel = new Button(220, 100, "CANCEL", "red").center(pane).mov(-130);
var confirm = new Button(220, 100, "CONFIRM", "green").center(pane).mov(130);
cancel.on("click", function() {pane.hide();});
confirm.on("click", function() {zgo("http://zimjs.com")});
pane.show(); // pressing anywhere will close pane (see parameters for options)

// custom backing with ZIM Pizzazz 3
// up top link to https://d309knd7es5f10.cloudfront.net/pizzazz_03.js
new Pane({
	label:new Label({color:white, text:"STOP", size:50}),
	backing:pizzazz.makePattern({
		type:"stripes",
		colors:series([red,black]),
		rows:20
	}).alp(.8)
}).show();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 200) width of pane
height - (default 200) height of pane
label - (default null) an optional ZIM Label (or text for default label properties)
backgroundColor - (default "white") a css color for the background of the Pane
color - (default "black") a css color for the text color of the Pane
draggable - (default false) pass in true to drag the pane
resets - (default true) resets position to start on re-open - set to false to keep last position
modal - (default true) pane will close when user clicks off the pane - set to false to keep pane open
corner - (default 20) is the corner radius - set to 0 for no corner
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
backdropColor - (default rgba(0,0,0,.2)) the color of the background that fills the stage
shadowColor - (default rgba(0,0,0,.3)) set to -1 for no shadow
shadowBlur - (default 20) how blurred the shadow is if shadow is set
center - (default true) centers the pane
	if center is false you will have to set x and y for the pane
	the registration point and the origin inside the pane is in the center
	you can adjust the label placement by changing its x and y or registration point
displayClose - (default true) closes the Pane if display backing is pressed
	if draggable is set to true, displayClose will automatically be set to false
backdropClose - (default true) closes the Pane if backdrop is pressed
backing - (default null) a Display object for the backing of the pane (eg. Shape, Bitmap, Container, Sprite)
	see ZIM Pizzazz module for a fun set of Shapes like Boomerangs, Ovals, Lightning Bolts, etc.
	as well as patterned backings using Pizzazz 3
fadeTime - (default 0) milliseconds to fade in and out
container - (default - the default stage) container for the pane
titleBar - (default null - no titleBar) a String or ZIM Label title for the pane that will be presented on a titleBar across the top
titleBarColor - (default "black") the color of the titleBar text if a titleBar is requested
titleBarBackgroundColor - (default "rgba(0,0,0,.2)") the background color of the titleBar if a titleBar is requested
titleBarHeight - (default fit label) the height of the titleBar if a titleBar is requested
close - (default false) - a close X for the top right corner that closes the pane when pressed
closeColor - (default #555) - the color of the close X if close is requested
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
show() - shows the pane (returns the pane for chaining)
hide() - hides the pane
toggle(state - default null) - shows if hidden and hides if showing (returns the pane for chaining)
	or pass in true to show pane or false to hide pane
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied (returns the new pane for chaining)
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
backing - or display - reference to the pane box
text - gives access to the label text
label - gives access to the label
titleBar - (default null - no titleBar) a String or ZIM Label title for the window that will be presented on a titleBar across the top
titleBarColor - (default "black") the text color of the titleBar if a titleBar is requested
titleBarBackgroundColor - (default "rgba(0,0,0,.2)") the background color of the titleBar if a titleBar is requested
titleBarHeight - (default fit label) the height of the titleBar if a titleBar is requested
toggled - read-only Boolean property as to whether pane is showing
close - access to the ZIM Shape if there is a close X
backdrop - reference to the backdrop that covers the stage
container - get or set the container the pane will be added to
resetX - if reset is true you can dynamically adjust the position if needed
resetY - and the y position for reset...
enabled - set to false to disable component
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties
and stage.update() in change event to see component change its graphics

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)

EVENTS
dispatches a "close" event when closed by clicking on backing, display, close, etc. when applicable

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+58
	zim.Pane = function(width, height, label, backgroundColor, color, draggable, resets, modal, corner, backdropColor, shadowColor, shadowBlur, center, displayClose, backdropClose, backing, fadeTime, container, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, close, closeColor, style, group, inherit) {
		var sig = "width, height, label, backgroundColor, color, draggable, resets, modal, corner, backdropColor, shadowColor, shadowBlur, center, displayClose, backdropClose, backing, fadeTime, container, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, close, closeColor, style, group, inherit";
		var duo; if (duo = zob(zim.Pane, arguments, sig, this)) return duo;
		z_d("58");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Pane";

		var mess = "zim display - Pane(): Please pass in a reference to a container with bounds set as first parameter";
		if (zot(container)) {
			if (zimDefaultFrame) {
				container = zimDefaultFrame.stage;
			} else {
				zog(mess);
				return;
			}
		} else if (!container.getBounds) {
			zog(mess);
			return;
		} else if (zot(container.getStage)) {
			zog("zim display - Pane(): The container must have a stage property");
			return;
		}

		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(width)) width=DS.width!=null?DS.width:200;
		if (zot(height)) height=DS.height!=null?DS.height:200;
		if (zot(label)) label = DS.label!=null?DS.label:null;
		if (typeof label === "string" || typeof label === "number") label = new zim.Label({
			text:label, size:DS.size!=null?DS.size:40, align:DS.align!=null?DS.align:"center", valign:DS.valign!=null?DS.valign:"center", color:DS.color!=null?DS.color:color,
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
			group:this.group
		});
		if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"white";
		if (zot(draggable)) draggable=DS.draggable!=null?DS.draggable:false;
		if (zot(resets)) resets=DS.resets!=null?DS.resets:true;
		if (zot(modal)) modal=DS.modal!=null?DS.modal:true;
		if (zot(corner)) corner=DS.corner!=null?DS.corner:20;

		if (zot(titleBar)) titleBar = DS.titleBar!=null?DS.titleBar:null;
		if (zot(titleBarColor)) titleBarColor = DS.titleBarColor!=null?DS.titleBarColor:null;
		if (zot(titleBarBackgroundColor)) titleBarBackgroundColor = DS.titleBarBackgroundColor!=null?DS.titleBarBackgroundColor:null;
		if (zot(titleBarHeight)) titleBarHeight = DS.titleBarHeight!=null?DS.titleBarHeight:null;

		if (zot(backdropColor)) backdropColor=DS.backdropColor!=null?DS.backdropColor:"rgba(0,0,0,.2)";
		if (zot(shadowColor))  shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:20;
		if (zot(center)) center=DS.center!=null?DS.center:true;
		if (zot(displayClose)) displayClose=DS.displayClose!=null?DS.displayClose:true;
		if (draggable) displayClose = false;
		if (zot(backdropClose)) backdropClose=DS.backdropClose!=null?DS.backdropClose:true;
		if (zot(fadeTime)) fadeTime=DS.fadeTime!=null?DS.fadeTime:0;
		if (zot(close)) close=DS.close!=null?DS.close:false;
		if (zot(closeColor)) closeColor=DS.closeColor!=null?DS.closeColor:"#555";

		var backdrop = this.backdrop = new zim.Shape({style:false});
		backdrop.type = "CreateJS_Shape";
		// make a big backing that closes the pane when clicked
		// could also provide a close button
		var g = backdrop.graphics;
		g.f(backdropColor);
		g.drawRect(-5000,-5000,10000,10000);
		// makes it seem like the pane has the dimensions of the display
		this.setBounds(-width/2,-height/2, width, height);

		var that = this;
		that.container = container;
		backdrop.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", backdropClose?closePane:function(e){e.stopImmediatePropagation();});
		var htmlList = new zim.Dictionary(true);
		function closePane(e) {
			removePane();
			that.container.stage.update();
			that.dispatchEvent("close");
			e.stopImmediatePropagation();
		};
		backdrop.on("mousedown", function(e) {
			e.stopImmediatePropagation();
		});
		if (modal) this.addChild(backdrop);

		var display;
		if (zot(backing)) {
			display = this.backing = this.display = new zim.Rectangle({
				width:width, height:height, color:backgroundColor, corner:corner, style:false
			});
		} else {
			if (backing.type == "Pattern") {
				var pattern = backing;
				// width, height, color, borderColor, borderWidth, corner, dashed, strokeObj, style, group, inherit
				display = new zim.Rectangle(width, height, backgroundColor, null, null, corner, null, null, false);
				pattern.centerReg(display);
				pattern.setMask(display.shape);
			} else {
				display = backing;
			}
			that.display = that.backing = display;
		}
		if (displayClose) {
			display.cursor = "pointer";
			display.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", closePane);
		}
		if (shadowColor != -1 && shadowBlur > 0) display.shadow = new createjs.Shadow(shadowColor, 8, 8, shadowBlur);
		display.on("click", function(e) {
			// stops the click from going through the display to the background
			e.stopImmediatePropagation();
		});

		this.resetX; this.resetY;
		if (draggable) {
			display.cursor = "pointer";
			var diffX, diffY;
			var stage;
			display.on("mousedown", function(e) {
				stage = e.target.stage;
				if (isNaN(that.resetX)) that.resetX = that.x;
				if (isNaN(that.resetY)) that.resetY = that.y;
				diffX = e.stageX/stage.scaleX - that.x;
				diffY = e.stageY/stage.scaleY - that.y;
				display.cursor = "pointer";
			});

			display.on("pressmove", function(e) {
				var p = checkBounds(e.stageX/stage.scaleX-diffX, e.stageY/stage.scaleY-diffY);
				that.x = p.x;
				that.y = p.y;
				var ch;
				for (var i=0; i<that.numChildren; i++) {
					ch = that.getChildAt(i);
					if (ch.type == "TextArea" || ch.type == "Loader" || ch.type == "Tag") {
						ch.resize();
					}
				}
				stage.update();
			});

			this.on("pressup", function(e) {
				display.cursor = "pointer";
				if (that.stage) that.stage.update();
			});
		}

		display.centerReg(this);

		if (label) {
			this.addChild(label);
			zim.center(label, this);
			this.label = label;
			this.text = label.text;
			label.mouseEnabled = false;
		}

		if (!zot(titleBar)) {
			if (typeof titleBar == "string") titleBar = new zim.Label(titleBar, null, null, titleBarColor);
			titleBarLabel = that.titleBarLabel = titleBar;
			if (zot(titleBarHeight)) titleBarHeight=titleBarLabel.height * 1.5;
			if (zot(titleBarColor)) titleBarColor = "black";
			if (zot(titleBarBackgroundColor)) titleBarBackgroundColor = "rgba(0,0,0,.2)";
			that.titleBar = titleBar = new zim.Container(width, titleBarHeight, null, null, false).centerReg(that).mov(0,-height/2+titleBarHeight/2);
			titleBar.mouseEnabled = false;
			titleBar.mouseChildren = false;
			var titleBarRect = that.titleBar.backing = new zim.Rectangle(width, titleBarHeight, titleBarBackgroundColor, null, null, [corner*.95,corner*.95, 0,0], null, null, false).addTo(titleBar);
			titleBarLabel.center(titleBar).pos({x:Math.max(corner/2, 10), reg:true});
		}

		if (close) {
			var close = that.close = new zim.Shape(-40,-40,80,80,null,false);
			close.graphics.f(closeColor).p("AmJEVIEUkTIkXkWIB4h5IEWEYIETkTIB4B3IkTESIEQERIh4B4IkRkRIkSEVg"); // width about 90 reg in middle
			if (titleBar) close.addTo(that).scaleTo(titleBar, null, 50).mov(width/2-Math.max(corner/2, 10)-close.width/2, -height/2+titleBarHeight/2).expand(40);
			else close.addTo(that).sca(.3).mov(width/2-close.width-3, -height/2+close.height).expand(40);
			close.cursor = "pointer";
			close.expand();
			close.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", closePane);
		}

		Object.defineProperty(that, 'text', {
			get: function() {
				var t = (label.text == " ") ? "" : label.text;
				return t;
			},
			set: function(value) {
				label.text = value;
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
			}
		});

		this.hide = function() {
			removePane();
			that.toggled = false;
			return that;
		}

		function removePane() {
			if (fadeTime > 0) {
				that.animate({obj:{alpha:0}, time:fadeTime, call:end});
			} else {
				end();
			}
			function end() {
				that.container.removeChild(that);
				var ch;
				for (var i=0; i<that.numChildren; i++) { // record depths first
					ch = that.getChildAt(i);
					if (ch.type == "TextArea" || ch.type == "Loader" || ch.type == "Tag") {
						var obj = {obj:ch, depth:that.getChildIndex(ch)};
						htmlList.add(ch, obj);
					}
				}
				for (var i=that.numChildren-1; i>=0; i--) { // remove textareas and loaders second
					ch = that.getChildAt(i);
					if (ch.type == "TextArea" || ch.type == "Loader" || ch.type == "Tag") {
						that.removeChild(ch);
					}
				}
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE))) that.container.stage.update();
				if (resets) {
					if (!isNaN(that.resetX)) that.x = that.resetX;
					if (!isNaN(that.resetY)) that.y = that.resetY;
				}

				if (that.zimAccessibility) {
					var a = that.zimAccessibility;
					a.resize(that);
					if (accessibilityClicker) accessibilityClicker.focus();
					else that.zimTabTag.nextSibling.focus();
					setTimeout(function() {a.talk("Pane has been closed.");}, 50);
				}
			}
		}

		var accessibilityClicker;
		this.show = function() {
			if (center) {
				if (isNaN(that.resetX)) {
					that.x = (that.container.getBounds().width) /2;
					that.y = (that.container.getBounds().height) /2;
				}
			}
			that.container.addChild(that);
			for (var i=0; i<htmlList.length; i++) {
				that.addChildAt(htmlList.values[i].obj, htmlList.values[i].depth);
			}
			if (fadeTime > 0) {
				that.alpha = 0;
				that.animate({alpha:1}, fadeTime);
			} else {
				if (that.container.stage) that.container.stage.update();
			}
			if (that.zimAccessibility) {
				var a = that.zimAccessibility;
				setTimeout(function(){if (a.activatedObject) accessibilityClicker = a.activatedObject.zimTabTag;}, 50);
				a.resize(that);
				a.tabIndex = that.zimTabIndex;
			}
			that.toggled = true;
			return that;
		}
		function checkBounds(x,y) {
			x = Math.max(width/2, Math.min(that.container.getBounds().width-width/2, x));
			y = Math.max(height/2, Math.min(that.container.getBounds().height-height/2, y));
			return {x:x,y:y}
		}

		this.toggle = function(state) {
			if (state===true) that.show();
			else if (state===false) that.hide();
			else if (that.container.contains(that)) that.hide();
			else that.show();
			return that;
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			var lX = label.x; // new Panes automatically center the label
			var lY = label.y;
			var p2 = that.cloneProps(new zim.Pane(width, height, label.clone(), backgroundColor, color, draggable, resets, modal, corner, backdropColor, shadowColor, shadowBlur, center, displayClose, backdropClose, zot(backing)?backing.clone():null, fadeTime, that.container, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, close, closeColor, style, this.group, inherit));
			p2.label.x = lX;
			p2.label.y = lY;
			return p2;
		}
	}
	zim.extend(zim.Pane, zim.Container, "clone", "zimContainer", false);
	//-58

/*--
zim.Window = function(width, height, backgroundColor, borderColor, borderWidth, padding, corner, swipe, scrollBarActive, scrollBarDrag, scrollBarColor, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, slide, slideDamp, slideSnap, interactive, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, scrollWheel, damp, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, cancelCurrentDrag, style, group, inherit)

Window
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Adds a window for content that can be swiped and scrolled.
NOTE: if zim namespace zns = true then this overwrites a JS Window - so the JS Window is stored as document.window

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var win = new Window({
	height:300,
	interactive:false,
	padding:0,
	slideDamp:.2
});
var container = new Container(); // make some content
var c; spacing = 10;
for (var i=0; i<4; i++) {
	c = frame.makeCircles();
	c.x = win.width/2;
	c.y = c.width/2 + (c.width+spacing)*i;
	container.addChild(c);
}
win.add(container); // add the content to the window
win.center(stage);
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 300) the width of the window
height - (default 200) the height of window - including the titleBar if there is a titleBar
backgroundColor - (default #333) background color (use clear - or "rbga(0,0,0,0)" for no background)
borderColor - (default #999) border color
borderWidth - (default 1) the thickness of the border
padding - (default 0) places the content in from edges of border (see paddingHorizontal and paddingVertical)
	padding is ignored if content x and y not 0 - and really only works on top left - so more like an indent
corner - (default 0) is the rounded corner of the window (does not accept corner array - scrollBars are too complicated)
swipe - (default auto/true) the direction for swiping set to none / false for no swiping
	also can set swipe to just vertical or horizontal
scrollBarActive - (default true) shows scrollBar (set to false to not)
scrollBarDrag - (default false) set to true to be able to drag the scrollBar
scrollBarColor - (default borderColor) the color of the scrollBar
scrollBarAlpha - (default .3) the transparency of the scrollBar
scrollBarFade - (default true) fades scrollBar unless being used
scrollBarH - (default true) if scrolling in horizontal is needed then show scrollBar
scrollBarV - (default true) if scrolling in vertical is needed then show scrollBar
slide - (default true) Boolean to throw the content when drag/swipe released
slideDamp - (default .6) amount the slide damps when let go 1 for instant, .01 for long slide, etc.
slideSnap - (default "vertical") "auto" / true, "none" / false, "horizontal"
	slides past bounds and then snaps back to bounds when released
	vertical snaps when dragging up and down but not if dragging horizontal
interactive - (default true) allows interaction with content in window
	set to false and whole window will be swipeable but not interactive inside
shadowColor - (default rgba(0,0,0,.3)) the color of the shadow
shadowBlur - (default 20) set shadowBlur to -1 for no drop shadow
paddingHorizontal - (default padding) places content in from top bottom (ignored if content x not 0)
paddingVertical - (default padding) places content in from left and right (ignored if content y not 0)
scrollWheel - (default true) scroll vertically with scrollWheel
damp - (default null) set to .1 for instance to damp the scrolling
titleBar - (default null - no titleBar) a String or ZIM Label title for the window that will be presented on a titleBar across the top
titleBarColor - (default "black") the text color of the titleBar if a titleBar is requested
titleBarBackgroundColor - (default "rgba(0,0,0,.2)") the background color of the titleBar if a titleBar is requested
titleBarHeight - (default fit label) the height of the titleBar if a titleBar is requested
draggable - (default true if titleBar) set to false to not allow dragging titleBar to drag window
boundary - (default null) set to ZIM Boundary() object - or CreateJS.rectangle()
close - (default false) - a close X for the top right corner that closes the window when pressed
closeColor - (default #555) - the color of the close X if close is requested
cancelCurrentDrag - (default false) - set to true to cancel window dragging when document window loses focus
	this functionality seems to work except if ZIM is being used with Animate - so we have left it turned off by default
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
add(obj, replace) - adds obj to content container of window (at padding) must have bounds set
	it is best to position and size obj first before adding
	otherwise if adjusting to outside current content size then call update()
	replace defaults to false and if set to true, removes all content then adds the obj.
	returns window for chaining
remove(obj) - removes object from content container of window and updates - returns window for chaining
removeAll() - removes all objects from content container of window and updates - returns window for chaining
resize(width, height) - resizes the Window without scaling the content (also calls update() for scroll update)
	width and height are optional - returns window for chaining
update() - resets window scrolling if perhaps the content gets bigger or smaller
cancelCurrentDrag() - stop current drag on window - but add dragging back again for next drag
clone(recursive) - makes a copy with properties such as x, y, etc. also copied
	recursive (default true) clones the window content as well (set to false to not clone content)
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
** see also the resize(width, height) method to resize the window without resizing the content
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
backing - CreateJS Shape used for backing of Window
content - ZIM Container used to hold added content
scrollBar - data object that holds the following properties (with defaults):
	you can set after object is made...
	scrollBar.horizontal = zim Shape // the horizontal scrollBar rectangle shape
	scrollBar.vertical = zim Shape // the vertical scrollBar rectangle shape
	scrollBar.color = borderColor; // the color of the scrollBar
	scrollBar.size = 6; // the width if vertical or the height if horizontal
	scrollBar.minSize = 12; // for the height if vertical or the width if horizontal
	scrollBar.spacing = 3 + size + borderWidth / 2;
	scrollBar.margin = 0; // adds extra space only at end by scrollBars
	scrollBar.corner = scrollBar.size / 2;
	scrollBar.showTime = 500; // ms to fade in
	scrollBar.fadeTime = 3000; // ms to fade out
scrollX - gets and sets the content x position in the window (this will be negative)
scrollY - gets and sets the content y position in the window (this will be negative)
scrollXMax - gets the max we can scroll in x based on content width - window width (plus padding and margin)
scrollYMax - gets the max we can scroll in y based on content height - window height (plus padding and margin)
titleBar - access to the ZIM Container for the titleBar if there is a titleBar
titleBarLabel - access to the ZIM Label of the titleBar if there is a titleBar
titleBarBacking - access to the ZIM Rectangle of the titleBar if there is a titleBar
close - access to the ZIM Shape if there is a close X
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "select" event when clicked on in a traditional manner (fast click with little movement)
dispatches a "hoverover" event when rolled on without moving for 300 ms
dispatches a "hoverout" event when not hovering due to movement or mouseout on the window
dispatches a "scrolling" event when the window scrolls
dispatches a "close" event when the window is closed with the x on the titleBar if there is a titleBar

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+58.1
	zim.Window = function(width, height, backgroundColor, borderColor, borderWidth, padding, corner, swipe, scrollBarActive, scrollBarDrag, scrollBarColor, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, slide, slideDamp, slideSnap, interactive, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, scrollWheel, damp, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, cancelCurrentDrag, style, group, inherit) {
		var sig = "width, height, backgroundColor, borderColor, borderWidth, padding, corner, swipe, scrollBarActive, scrollBarDrag, scrollBarColor, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, slide, slideDamp, slideSnap, interactive, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, scrollWheel, damp, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, cancelCurrentDrag, style, group, inherit";
		var duo; if (duo = zob(zim.Window, arguments, sig, this)) return duo;
		z_d("58.1");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Window";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(width)) width=DS.width!=null?DS.width:300;
		if (zot(height)) height=DS.height!=null?DS.height:200;
		if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"#333";
		var originalBorderColor = borderColor;
		var originalBorderWidth = borderWidth;
		if (zot(borderColor)) borderColor=DS.borderColor!=null?DS.borderColor:"#999";
		if (zot(borderWidth)) borderWidth=DS.borderWidth!=null?DS.borderWidth:1; // 0
		if (zot(padding)) padding=DS.padding!=null?DS.padding:0;
		if (zot(corner)) corner=DS.corner!=null?DS.corner:0;
		if (zot(swipe)) swipe=DS.swipe!=null?DS.swipe:true; // true / auto, vertical, horizontal, false / none
		if (zot(scrollBarActive)) scrollBarActive=DS.scrollBarActive!=null?DS.scrollBarActive:true;
		if (zot(scrollBarDrag)) scrollBarDrag=DS.scrollBarDrag!=null?DS.scrollBarDrag:false;
		if (zot(scrollBarColor)) scrollBarColor=DS.scrollBarColor!=null?DS.scrollBarColor:borderColor;
		if (zot(scrollBarAlpha)) scrollBarAlpha=DS.scrollBarAlpha!=null?DS.scrollBarAlpha:.3;
		if (zot(scrollBarFade)) scrollBarFade=DS.scrollBarFade!=null?DS.scrollBarFade:true;
		if (zot(scrollBarH)) scrollBarH = DS.scrollBarH!=null?DS.scrollBarH:true;
		if (zot(scrollBarV)) scrollBarV = DS.scrollBarV!=null?DS.scrollBarV:true;
		if (scrollBarDrag) scrollBarFade = DS.scrollBarFade!=null?DS.scrollBarFade:false;
		if (zot(slide)) slide=DS.slide!=null?DS.slide:true;
		if (zot(slideDamp)) slideDamp=DS.slideDamp!=null?DS.slideDamp:.6;
		if (zot(slideSnap)) slideSnap=DS.slideSnap!=null?DS.slideSnap:"vertical"; // true / auto, vertical, horizontal, false / none
		if (zot(interactive)) interactive=DS.interactive!=null?DS.interactive:true;
		if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:20;
		if (zot(paddingVertical)) paddingVertical=DS.paddingVertical!=null?DS.paddingVertical:padding;
		if (zot(paddingHorizontal)) paddingHorizontal=DS.paddingHorizontal!=null?DS.paddingHorizontal:padding;
		if (zot(scrollWheel)) scrollWheel = DS.scrollWheel!=null?DS.scrollWheel:true;
		if (zot(titleBar)) titleBar = DS.titleBar!=null?DS.titleBar:null;
		if (zot(titleBarColor)) titleBarColor = DS.titleBarColor!=null?DS.titleBarColor:null;
		if (zot(titleBarBackgroundColor)) titleBarBackgroundColor = DS.titleBarBackgroundColor!=null?DS.titleBarBackgroundColor:null;
		if (zot(titleBarHeight)) titleBarHeight = DS.titleBarHeight!=null?DS.titleBarHeight:null;
		if (zot(draggable)) draggable = DS.draggable!=null?DS.draggable:null;
		if (zot(boundary)) boundary = DS.boundary!=null?DS.boundary:null;
		if (zot(close)) close = DS.close!=null?DS.close:null;
		if (zot(closeColor)) closeColor = DS.closeColor!=null?DS.closeColor:null;
		if (zot(cancelCurrentDrag)) cancelCurrentDrag = DS.cancelCurrentDrag!=null?DS.cancelCurrentDrag:false;

		if (titleBar === false) titleBar = null;
		if (!zot(titleBar)) {
			if (zot(titleBarHeight)) titleBarHeight = 30;
			height = height - titleBarHeight;
		}

		var that = this;
		this.scrollX = this.scrollY = this.scrollXMax = this.scrollYMax = 0;

		var backing = this.backing = new zim.Shape({style:false});
		this.addChild(backing);

		var mask = new createjs.Shape();
		mask.type = "WindowBacking";
		var mg = mask.graphics;
		// make the mask in the update function
		// when we know if there are vertical and horizontal scrollBars
		this.addChild(mask);

		var content = this.content = new zim.Container({style:false});
		this.addChild(content);
		content.mask = mask;
		var stage;

		if (!interactive) {
			// hitArea makes the whole window draggable
			// but then you can't interact with the content inside the window
			var hitArea = new createjs.Shape();
		}
		if (borderWidth > 0) {
			var border = new createjs.Shape();
			this.addChild(border);
		}

		var titleBarCorner = titleBar?0:corner;

		// we call this function at start and when resize() is called to resize the window without scaling content
		function sizeWindow() {
			that.setBounds(0,0,width,height);
			backing.graphics.c().f(backgroundColor).rc(0,0,width,height,titleBarCorner,titleBarCorner,corner,corner);
			if (shadowColor != -1 && shadowBlur > 0) backing.shadow = new createjs.Shadow(shadowColor, 4, 4, shadowBlur);

			if (borderWidth > 0) {
				if (corner) {
					border.graphics.c().s(borderColor).ss(borderWidth, "square", "miter").rc(0,0,width,height,titleBarCorner,titleBarCorner,corner,corner);
				} else {
					border.graphics.c().s(borderColor).ss(borderWidth, "square", "miter").dr(0,0,width,height);
				}
			}
		}
		sizeWindow();

		// this exposes an scrollBar data object so creators can adjust scrollBar properties
		// note that these properties are set dynamically in the update function
		var scrollBar = this.scrollBar = {}; // data object to expose scrollBar properties
		scrollBar.color = scrollBarColor;
		scrollBar.size = 6;
		scrollBar.minSize = scrollBar.size*2; // if vertical scroll, this is vertical minSize where size is horizontal size
		scrollBar.spacing = 3.5 + borderWidth / 2;
		scrollBar.margin = 0;
		scrollBar.corner = scrollBar.size / 2;
		scrollBar.showTime = 500;
		scrollBar.fadeTime = 3000;

		if (scrollBarActive) {
			var hscrollBar = scrollBar.horizontal = new zim.Shape({style:false});
			var hg = hscrollBar.graphics;
			hscrollBar.alpha = scrollBarAlpha;
			this.addChild(hscrollBar);
			if (scrollBarDrag) hscrollBar.drag({localBounds: true});

			var vscrollBar = scrollBar.vertical = new zim.Shape({style:false});
			var vg = vscrollBar.graphics;
			vscrollBar.alpha = scrollBarAlpha;
			this.addChild(vscrollBar);
			if (scrollBarDrag) vscrollBar.drag({localBounds: true});
		}

		var hProportion;
		var vProportion;
		var hCheck;
		var vCheck;
		var gap;
		var contentWidth;
		var contentHeight;

		var hEvent;
		var vEvent;
		var dTimeout;

		this.update = function() {
			if (scrollBarActive) {
				// clear the scrollBars and remake anytime this function is called
				// as these may change as people add and remove content to the Window
				hg.clear(); // horizontal scrollBar
				vg.clear(); // vertical scrollBar
			}

			// assume no gap at left and top
			// gap is applied in x if there is a scroll in y
			// gap is applied in y if there is a scroll in x
			gap = (scrollBarActive) ? scrollBar.size+scrollBar.spacing*2 : 0;
			contentWidth = content.getBounds()?content.getBounds().width:0;
			contentHeight = content.getBounds()?content.getBounds().height:0;

			// note, the contentWidth and contentHeight include ONE padding
			hCheck = (scrollBarH && contentWidth > width-paddingHorizontal && (scrollBarActive || swipe === true || swipe == "auto" || swipe == "horizontal"));
			vCheck = (scrollBarV && contentHeight > height-paddingVertical && (scrollBarActive || swipe === true || swipe == "auto" || swipe == "vertical"));

			that.scrollXMax = contentWidth+paddingHorizontal*2-width+(vCheck?gap+scrollBar.margin:0);
            that.scrollYMax = contentHeight+paddingVertical*2-height+(hCheck?gap+scrollBar.margin:0);

			// set mask dynamically as scrollBars may come and go affecting the mask size slightly
			mg.clear();
			var xx = borderWidth/2;
			var yy = borderWidth/2;
			var ww = width-((vCheck && scrollBarActive)?scrollBar.size+scrollBar.spacing*2:0)-(vCheck?0:borderWidth);
			var hh = height-((hCheck && scrollBarActive)?scrollBar.size+scrollBar.spacing*2:0)-(hCheck?0:borderWidth);
			mg.f("rgba(0,0,0,0)").rc(xx,yy,ww,hh,titleBarCorner,titleBarCorner,vCheck&&scrollBarActive?0:corner,hCheck&&scrollBarActive?0:corner);

			mask.setBounds(that.getBounds().x,that.getBounds().y,that.getBounds().width, that.getBounds().height);
			zim.expand(mask, 0);
			if (!interactive) {
				hitArea.graphics.c().f("red").dr(xx,yy,ww,hh);
				content.hitArea = hitArea;
			}

			var edgeAdjust = Math.max(corner, Math.min(scrollBar.corner, scrollBar.spacing));
			var edgeLeft = edgeAdjust + borderWidth/2;
			var edgeRight = edgeAdjust + (vCheck?gap:0) + borderWidth/2;
			var edgeTop = edgeAdjust + borderWidth/2;
			var edgeBottom = edgeAdjust + (hCheck?gap:0) + borderWidth/2;

			if (hCheck && scrollBarActive) {
				scrollBarLength = Math.max(scrollBar.minSize, (width-edgeLeft-edgeRight) * (width-edgeLeft-edgeRight) / (contentWidth + paddingHorizontal + scrollBar.margin));
				hg.f(scrollBar.color).rr(0,0,scrollBarLength,scrollBar.size,scrollBar.corner);
				hscrollBar.x = edgeLeft;
				hscrollBar.y = height-scrollBar.size-scrollBar.spacing;
				// for swiping window:
				hProportion = new zim.Proportion(-that.scrollXMax, 0, edgeLeft, width-scrollBarLength-edgeRight, -1);
				if (scrollBarDrag) {
					hscrollBar.setBounds(0,0,scrollBarLength,scrollBar.size);
					// drag rect for scrollBar
					var rect = new createjs.Rectangle(
						edgeLeft, hscrollBar.y, width-scrollBarLength-edgeLeft-edgeRight, 0
					);
					hscrollBar.dragBoundary(rect);
					hscrollBar.proportion = new zim.Proportion(
						rect.x, rect.x+rect.width, 0, -that.scrollXMax
					);
					hscrollBar.off("pressmove", hEvent);
					hEvent = hscrollBar.on("pressmove", function() {
						that.dispatchEvent("scrolling");
						content.x = hscrollBar.proportion.convert(hscrollBar.x);
					});
				}
			}

			if (vCheck && scrollBarActive) {
				scrollBarLength = Math.max(scrollBar.minSize, (height-edgeTop-edgeBottom) * (height-edgeTop-edgeBottom) / (contentHeight + paddingVertical + scrollBar.margin));
				vg.f(scrollBar.color).rr(0,0,scrollBar.size,scrollBarLength,scrollBar.corner);
				vscrollBar.x = width-scrollBar.size-scrollBar.spacing;
				vscrollBar.y = edgeTop;
				// for swiping window:
				vProportion = new zim.Proportion(-that.scrollYMax, 0, edgeTop, height-scrollBarLength-edgeBottom, -1);
				if (scrollBarDrag) {
					vscrollBar.setBounds(0,0,scrollBar.size,scrollBarLength);
					// drag rect for scrollBar
					var rect = new createjs.Rectangle(
						vscrollBar.x, edgeTop, 0, height-scrollBarLength-edgeTop-edgeBottom
					);
					vscrollBar.dragBoundary(rect);
					vscrollBar.proportion = new zim.Proportion(
						rect.y, rect.y+rect.height, 0, -that.scrollYMax
					);
					vscrollBar.off("pressmove", vEvent);
					vEvent = vscrollBar.on("pressmove", function() {
						that.dispatchEvent("scrolling");
						desiredY = content.y = vscrollBar.proportion.convert(vscrollBar.y);
					});
				}
			}
			movescrollBars();

			clearTimeout(that.d2Timeout);
			that.d2Timeout = setTimeout(function(){
				if (hscrollBar && hscrollBar.proportion) content.x = hscrollBar.proportion.convert(hscrollBar.x);
				if (vscrollBar && vscrollBar.proportion) content.y = vscrollBar.proportion.convert(vscrollBar.y);
			}, 50);
			clearTimeout(that.dTimeout);
			that.dTimeout = setTimeout(function(){setdragBoundary();}, 300);
		}

		this.resize = function(w, h) {
			if (zot(w)) w = width;
			if (zot(h)) h = height;
			width = w;
			height = h;
			sizeWindow();
			that.update();
			desiredY = content.y;
			if (damp) dampY.immediate(desiredY);
			return that;
		}

		if (!zot(titleBar)) {
			if (zot(draggable)) draggable = true;
			if (typeof titleBar == "string") titleBar = new zim.Label({
				text:titleBar, color:titleBarColor, size:DS.size!=null?DS.size:20,
				backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
				group:this.group
			});
			titleBarLabel = that.titleBarLabel = titleBar;
			if (zot(titleBarBackgroundColor)) titleBarBackgroundColor = "rgba(0,0,0,.2)";
			that.titleBar = titleBar = new zim.Container(width, titleBarHeight, null, null, false).centerReg(that).mov(0,-height/2-titleBarHeight/2);
			var titleBarRect = that.titleBar.backing = new zim.Rectangle(width+borderWidth, titleBarHeight, titleBarBackgroundColor, null, null, [corner*.95, corner*.95, 0, 0], true, null, false).center(titleBar);
			titleBarLabel.center(titleBar).pos({x:Math.max(corner/2, Math.max(10, padding)), reg:true});
			that.regX = 0; that.regY = -titleBarHeight;
			that.setBounds(0,-titleBarHeight,width,height+titleBarHeight);

			if (draggable) {
				titleBar.cursor = "pointer";
				titleBar.on("mousedown", function() {
					that.drag({rect:boundary, currentTarget:true});
				});
				titleBar.on("pressup", function() {
					that.noDrag();
				});
			} else {
				titleBar.on("mousedown", function () {});
			}
		}

		if (close) {
			if (zot(closeColor)) closeColor = "#555";
			var close = that.close = new zim.Shape(-40,-40,80,80,null,false);
			close.graphics.f(closeColor).p("AmJEVIEUkTIkXkWIB4h5IEWEYIETkTIB4B3IkTESIEQERIh4B4IkRkRIkSEVg"); // width about 90 reg in middle
			if (titleBar) close.centerReg(that).scaleTo(titleBar, null, 50).pos({x:width-Math.max(corner/2, Math.max(10, padding))-close.width/2, y:titleBarHeight/2, reg:true}).expand(40);
			else {
				close.sca(.3).pos((Math.max(corner/2, Math.max(10, padding)))/2, close.height/2, true, false, that).expand(40);
			}
			close.cursor = "pointer";
			close.expand();
			close.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", function(){
				that.removeFrom();
				that.dispatchEvent("close");
			});
		}

		// METHODS to add and remove content from Window
		this.add = function(c, replace) {
			makeDamp(c);
			if (!c.getBounds()) {zog("SwipeBox.add() - please add content with bounds set"); return;}
			if (replace) that.removeAll();
			content.addChild(c);
			if (c.x == 0) c.x = paddingHorizontal;
			if (c.y == 0) c.y = paddingVertical;
			that.update();
			return that;
		}

		this.remove = function(c) {
			content.removeChild(c);
			that.update();
			return that;
		}

		this.removeAll = function() {
			content.removeAllChildren();
			that.update();
			return that;
		}

		function setdragBoundary() {
			zim.dragBoundary(content, new createjs.Rectangle(0, 0, hCheck?-that.scrollXMax:0, vCheck?-that.scrollYMax:0));
		}

		var swipeCheck = false;
		if (swipe) {
			content.on("mousedown", function() {
				if (!swipeCheck) zim.Ticker.add(swipeMovescrollBars, content.stage);
				swipeCheck = true;
				if (hCheck && scrollBarActive) if (scrollBarFade) zim.animate(hscrollBar, {alpha:scrollBarAlpha}, scrollBar.showTime);
				if (vCheck && scrollBarActive) if (scrollBarFade) zim.animate(vscrollBar, {alpha:scrollBarAlpha}, scrollBar.showTime);
			});
		}

		function swipeMovescrollBars() {
			// this is being called by the swipe which has its own damping
			// so we need to set the desiredY and then move the scrollBars
			// as the movescrollBars needs to run independently - so both types of damp can controll it
			desiredY = content.y;
			if (damp) dampY.immediate(desiredY);
			if (scrollBarActive) movescrollBars();
		}

		function movescrollBars() {
			that.dispatchEvent("scrolling");
			if (hitArea) {
				// move hitarea to display box
				hitArea.x = -content.x;
				hitArea.y = -content.y;
			}
			if (hCheck && scrollBarActive) hscrollBar.x = hProportion.convert(content.x);
			if (vCheck && scrollBarActive) vscrollBar.y = vProportion.convert(content.y);
		}

		// may add content before adding Window to stage...
		this.on("added", setDrag, null, true); // once
		function setDrag() {
			makeDamp(that);
			if (!swipe) return;
			zim.drag({
				obj:content,
				currentTarget:true,
				localBounds:true,
				slide:slide, slideDamp:slideDamp,
				slideSnap:(scrollBarH && (swipe===true||swipe=="auto"||swipe=="horizontal")) || (scrollBarV && (swipe===true||swipe=="auto"||swipe=="vertical"))?slideSnap:false
			});
			if (content.getBounds() && content.getBounds().width > 0) {
				setTimeout(function(){setdragBoundary();}, 300);
			}
		}
		this.cancelCurrentDrag = function() {
			that.content.noDrag();
			zim.drag({
				obj:content,
				currentTarget:true,
				localBounds:true,
				slide:slide, slideDamp:slideDamp,
				slideSnap:(scrollBarH && (swipe===true||swipe=="auto"||swipe=="horizontal")) || (scrollBarV && (swipe===true||swipe=="auto"||swipe=="vertical"))?slideSnap:false
			});
			if (content.getBounds() && content.getBounds().width > 0) {
				setTimeout(function(){setdragBoundary();}, 300);
			}
		}

		if (interactive) {
			this.added(function (theStage) {
				theStage.on("stagemousemove", function (e) {
					that.windowMouseX = e.stageX/theStage.scaleX;
					that.windowMouseY = e.stageY/theStage.scaleY;
				});
			});
		}

		if (slide) {
			content.on("slidestop", stageUp);
		} else {
			content.on("mousedown", function() {
				content.stage.on("stagemouseup", stageUp, null, true);
			});
		}
		if (cancelCurrentDrag) {
			that.blurEvent = document.window.addEventListener("blur", function () {
				that.cancelCurrentDrag();
				stageUp();
			});
		}

		function stageUp(e) {
			zim.Ticker.remove(swipeMovescrollBars);
			swipeCheck = false;
			if (hCheck) if (scrollBarFade) zim.animate(hscrollBar, {alpha:0}, scrollBar.fadeTime);
			if (vCheck) if (scrollBarFade) zim.animate(vscrollBar, {alpha:0}, scrollBar.fadeTime);
		}

		if (interactive) {
			// dispatches SELECT (click) and HOVEROVER (500 ms) and gives mouseX and mouseY on content
			// CLICKS (in the traditional sense rather than a mouseup replacement)
			var downLoc;
			var downTime;

			content.on("mousedown", function(e){stage=e.target.stage; downLoc=e.stageX/stage.scaleX; downTime=Date.now();});
			content.on("click", function(e){
				if (Date.now()-downTime<600 && Math.abs(e.stageX/stage.scaleX-downLoc)<5) {
					that.contentMouse = content.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
					that.dispatchEvent("select");
				}
			});
			// HOVER (must stay within thresh pixels for pauseTime ms)
			content.on("mouseover", moveOn);
			content.on("mouseout", moveOff);
			var startTime;
			function moveOn() {
				startTime=Date.now();
				zim.Ticker.add(timeMouse, content.stage);
			}
			function moveOff() {
				if (!hoverOutCalled) {
					that.dispatchEvent("hoverout");
					hoverOutCalled = true;
				}
				zim.Ticker.remove(timeMouse);
			}
			var lastMouseX = 0;
			var lastMouseY = 0;
			var lastReportX = 0;
			var lastReportY = 0;
			var pauseTime = 300;
			var thresh = 2;
			var hoverOutCalled = false;
			function timeMouse() {
				if (!content.stage) {
					if (!hoverOutCalled) {
						that.dispatchEvent("hoverout");
						hoverOutCalled = true;
					}
					zim.Ticker.remove(timeMouse);
					return;
				}
				if (Math.abs(lastMouseX-that.windowMouseX) > thresh || Math.abs(lastMouseY-that.windowMouseY) > thresh) {
					if (!hoverOutCalled) {
						that.dispatchEvent("hoverout");
						hoverOutCalled = true;
					}
					startTime=Date.now();
					lastMouseX=that.windowMouseX;
					lastMouseY=that.windowMouseY;
				} else {
					if (Date.now()-startTime > pauseTime) {
						if (Math.abs(lastReportX-that.windowMouseX) > thresh || Math.abs(lastReportY-that.windowMouseY) > thresh) {
							that.contentMouse = content.globalToLocal(that.windowMouseX, that.windowMouseY);
							that.dispatchEvent("hoverover");
							lastReportX=that.windowMouseX;
							lastReportY=that.windowMouseY;
							hoverOutCalled = false;
						}
						startTime=Date.now();
					}
				}
			}
		}

		var scrollEvent1;
		var scrollEvent2;
		var scrollEvent3;
		var desiredY = that.scrollY;
		if (scrollWheel) {
			scrollEvent1 = window.addEventListener("mousewheel", scrollWindow);
			scrollEvent2 = window.addEventListener("wheel", scrollWindow);
			scrollEvent3 = window.addEventListener("DOMMouseScroll", scrollWindow);
			function scrollWindow(e) {
				if (vCheck && that.stage && that.hitTestPoint(that.windowMouseX, that.windowMouseY) && that.contains(that.stage.getObjectUnderPoint(that.windowMouseX*that.stage.scaleX, that.windowMouseY*that.stage.scaleY))) {
					if (zot(e)) e = event;
					var delta = e.detail ? e.detail*(-19) : e.wheelDelta;
					if (zot(delta)) delta = e.deltaY*(-19);
					desiredY += delta;
					desiredY = Math.max(-that.scrollYMax, Math.min(0, desiredY))
					if (!damp) {
						that.scrollY = desiredY;
						content.stage.update();
					}
				}
			}
		}
		var dampCheck = false;
		var dampY;
		function makeDamp(obj) {
			if (damp && !dampCheck && obj.stage) {
				dampCheck = true;
				dampY = new zim.Damp(that.scrollY, damp);
				zim.Ticker.add(function() {
					if (swipeCheck) return;
					if (!zot(desiredY)) that.scrollY = dampY.convert(desiredY);
				}, obj.stage);
			}
		}

		Object.defineProperty(that, 'scrollX', {
			get: function() {
				return content.x;
			},
			set: function(value) {
				content.x = value;
				if (content.zimDragImmediate) content.zimDragImmediate(content.x, content.y);
				movescrollBars();
			}
		});

		Object.defineProperty(that, 'scrollY', {
			get: function() {
				return content.y;
			},
			set: function(value) {
				content.y = value;
				if (content.zimDragImmediate) content.zimDragImmediate(content.x, content.y);
				movescrollBars();
			}
		});

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function(recursive) {
			if (zot(recursive)) recursive = true;
			var w = that.cloneProps(new zim.Window(width, height, backgroundColor, originalBorderColor, originalBorderWidth, padding, corner, swipe, scrollBarActive, scrollBarDrag, scrollBar.color, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, slide, slideDamp, slideSnap, interactive, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, cancelCurrentDrag, style, group, inherit));
			if (recursive) {
				that.content.cloneChildren(w.content);
				w.update();
			}
			return w;
		}

		this.dispose = function() {
			if (scrollWheel) {
				window.removeEventListener("mousewheel", scrollEvent1);
				window.removeEventListener("wheel", scrollEvent2);
				window.removeEventListener("DOMMouseScroll", scrollEvent3);
			}
			if (that.blurEvent) document.window.removeEventListener("blur", that.blurEvent);
			zim.Ticker.remove(timeMouse);
			zim.Ticker.remove(swipeMovescrollBars);
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.Window, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-58.1

/*--
zim.Layer = function(width, height, titleBar, titleBarContainer, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, borderWidth, borderColor, dashed, transformObject, titleBarWidth, titleBarHeight, titleBarX, titleBarY, titleBarDraggable, close, closeColor, closeBackgroundColor, closeIndicatorColor, anchor, style, group, inherit)

Layer
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Layer is a ZIM Container with transform controls.
ZIM transform() objects have their mousechildren turned off so they can be dragged and transformed.
This means there can be no interactivity inside the transformed object.
Layer provides a solution to nest transformed objects in transformable containers.
It does so by providing a titleBar that can be used to turn on and off the transform of the container
and allow its contents to be transformed when the transform controls of the Layer are turned off.
This is more than just hiding the transform tools but rather removing and adding them.

The Layer titleBar will always remain visible on the stage
If the Layer is moved (not by its titleBar) so that the titleBar hits the edge,
then the titleBar will become anchored to the edge (unless anchor is set to false)
This creates an independent titleBar that can be moved to any location.
The titleBarPos() method can also be used to separate the titleBar at any time.
Drop the titleBar on the top left corner of the Layer or doubleClick it to snap it back on to the layer

NOTE: Layers can be added to a Transform Manager and saved with the persist sytem.
NOTE: Layers can be added to Layers (nested) along with any other type of DisplayObject content.
NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

SEE: https://zimjs.com/explore/layer.html

EXAMPLE
// adding the Layers above the content will allow pressing Layer titleBar objects inside other Layers
// adding everything right on the stage would not allow pressing titleBars inside other Layers - either way may be best, depending on content
var content = new Container(stageW, stageH).addTo();
var layers = new Container(stageW, stageH).addTo();

// create an outer layer with two inner layers - one holding a circle and the other two circles
var layer0 = new Layer(800, 500, "LAYER 0", layers).center(content);
var layer1 = new Layer(300, 400, "LAYER 1", layers).loc(50,50,layer0);
var circle1 = new Circle(50, pink).center(layer1).transform({visible:false});
var layer2 = new Layer(300, 400, "LAYER 2", layers).pos(50,50,true,false,layer0);
var circle2 = new Circle(50, green).center(layer2).mov(0, -80).transform({visible:false});
var circle3 = new Circle(50, blue).center(layer2).mov(0, 80).transform({visible:false});

// optionally store transforms
var t = new TransformManager([layer0, layer1, layer2, circle1, circle2, circle3], "layersID");
// t.clearPersist("layersID")

layer2.titleBarPos(30,30); // position a titleBar for instance
timeout(1000, function () {
	layer2.resetTitleBar();
	layer2.turnOn();

	// if moving manually, must call resize()
	layer2.mov(30);
	layer2.resize();
	stage.update();
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 500) the width of the Layer Container
height - (default 500) the height of the Layer Container not including the titleBar (which is not in the Container)
titleBar - (default "LAYER") a String or ZIM Label for the titleBar
titleBarContainer - (default null - ZimDefaultFrame' stage) a container for the titleBar
	can group these with other Layers and hide them all by hiding the container for instance
	this also can help layer the titleBars above the content
backgroundColor - (default #eee) the background color of the titleBar
rollBackgroundColor - (default #fff) the roll background color of the titleBar
selectedBackgroundColor - (default #666) the selected background color of the titleBar
color - (default #666) the color of the titleBar text
rollColor - (default #666) the roll color of the titleBar text
selectedColor - (default #ddd) the selected color of the titleBar text
borderWidth - (default 1) the width of the ghost outline when the Layer is not selected
	to adjust the transform controls border width use the transformObject parameter and set the borderWidth property
borderColor - (default borderColor) the color of the ghost outline when the Layer is not selected
	to adjust the transform controls border color use the transformObject parameter and set the borderColor property
dashed - (default true) the dashed of the ghost outline when the Layer is not selected
	to adjust the transform controls border dashed use the transformObject parameter and set the dashed property
transformObject - (default {borderColor:selectedBackgroundColor}) any of the transform parameters as an object literal
	certain properties are overwritten by Layer as follows:
	{events:true, visible:false, ghostColor:borderColor, ghostWidth:borderWidth, ghostDashed:dashed, ghostHidden:true}
	use the transformControls.show() to show the transform controls once the Layer is made for instance:
	timeout(100, function(){layer.transformControls.show();}); // a timeout is needed as Layer gets created - sorry.
titleBarWidth - (default 100 + 30 if close) the width of the titleBar.  30 pixels will be added if close is true
titleBarHeight - (default 40) the height of the titleBar
titleBarX - (default null) the starting x position of the titleBar - see also titleBarPos() and resetTitleBar() methods
titleBarY - (default null) the starting y position of the titleBar - see also titleBarPos() and resetTitleBar() methods
titleBarDraggable - (default true) set to false to not let the titleBar be dragged.
	this is useful with the titleBarPos() to create a stationary menu for the layers - for instance along the edge like tabs
close - (default true) - set to false to not use the close checkbox
	WARNING: without the close checkbox, the user may make the layer bigger than the stage and not be able to deselect the layer
closeColor - (default selectedBackgroundColor) the border of the close checkBox
closeBackgroundColor - (default selectedBackgroundColor) the backgroundColor of the close checkBox
closeIndicatorColor - (default selectedColor) the indicator color of the close checkBox
anchor - (default true) set to false to not anchor the titleBar to the edge if dragged with the Layer (not the titleBar)
	with anchor true, the user can dock the titleBar to the edges and then drag them to any desired location
	the user can snap the titleBar back on the layer by dropping it on the top left corner of the layer or double clicking the titleBar
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
titleBarPos(x, y, right, bottom) - position the titleBar in the titleBarContainer - returns object for chaining
	This will undock the titleBar from the Layer so it can be moved independently
	unless titleBarDraggable is set to false
	See also titleBarX and titleBarY parameters to start titleBars at a certain position
resetTitleBar() - dock the titleBar back on the Layer - returns object for chaining
toggle(state) - toggle the controls or turn on or off the controls by providing a Boolean state - returns object for chaining
resize(dispatch) - resize the Layer and its children if Layer is manually adjusted - returns object for chaining
	for instance, layer.x = 10; layer.resize(); otherwise, the transform controls are broken!
	normal layer transforming using the controls automatically resize.
	Setting dispatch to true will dispatch a "transformed" event
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied (returns the new waiter for chaining)
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
transformControls - the transform transformControls object - see below for a description
anchor - get or set whether the titleBar will anchor to the edges of the titleBarContainer
toggled - read only if Layer has its transform turned on - or use transformControls.visible
	use toggle(state) to toggle controls or pass in true for show controls or false for hide controls
titleBar - access to the ZIM Container that holds the titleBar
titleBarDraggable - get or set whether the titleBar can be dragged
	use with titleBarPos() to permanently positing the titleBar
checkBox - access to the ZIM CheckBox that shows when the Layer is active and close is true
button - access to the ZIM Button that makes up the titleBar
label - access to the ZIM Label that is on the Button for the titleBar
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the transformControls property described below for more options.
ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

TRANSFORM CONTROL OBJECT
Layer receives a transformControls property
This may be slightly delayed as Layer is prepared on stage
var layer = new Layer().center();
timeout(100, function(){zog(layer.transformControls);}); // will probably do the trick
The transformControls property holds the following:

TRANSFORM CONTROL OBJECT PROPERTIES
visible - read only whether the controls are visible
ghost - read only as to whether the ghost outline is showing - set with showGhost and hideGhost
ghostEnabled - read only as to whether the ghost outline will be turned on and off - set with addGhost and removeGhost
scaleControls - reference to the Container that holds the corner boxes for scaling
stretchXControls - reference to the Container that holds the left and right boxes for stretching
stretchYControls - reference to the Container that holds the top and bottom boxes for stretching
rotateControls - reference to the Container that holds the outer circles for rotating

TRANSFORM CONTROL OBJECT METHODS
hide() - hides the controls - returns object for chaining
show() - shows the controls - returns object for chaining
recordData(toJSON) - returns an object with type, x, y, scaleX, scaleY, rotation, skewX, skewY, visible PROPERTIES
	if toJSON (default false) is set to true, the return value is a JSON string
setData(data, fromJSON) - sets the properties to match the data object passed in - this should come from recordData()
	if fromJSON (default false) is set to true, it will assume a JSON string is passed in as data
	returns object for chaining
remove(noHide) - removes the controls - set noHide true if already hidden
add(noShow) - adds the controls back if then have been removed - set noShow true if not wanting to show
allowToggleOn() - sets the show / hide controls on with click
allowToggleOff() - removes the show / hide controls on with click
showGhost() - show the ghost outline - the ghostWidth or ghostColor must be set in initial parameters
hideGhost() - hide the ghost outline
toggleGhost(state) - if ghost is showing will hide ghost and if ghost is hidden will show ghost
	or set state to true to show ghost or false to not show ghost
addGhost() - enable ghost outline functionality - the ghostWidth or ghostColor must be set in initial parameters
removeGhost() - disable ghost outline functionality
disable() - may show the controls if visible but cannot use them
enable() - turns the using of the controls back on
resize(dispatch) - call resize if the object is transformed in ways other than with the controls
	set dispatch to true to dispatch a "transformed" event - if manually adjusted this will save to TransformManager

EVENTS
dispatches a "transformed" event when being transformed
	the transformed event object has a transformType property
	the transformType property has values of "size", "move", "rotate", "stretch", "reg", "reset"
	the transformType also might be "resize" if resize(true) is called to dispatch a transformed event
	the transformed event object also has a pressup property that is true if on pressup and null if from pressmove
dispatches "transformshow" and "transformhide" events for when click to hide or show controls
If TransformManager() is used there are more events available such as "persistset", etc.

See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+58.5
	zim.Layer = function(width, height, titleBar, titleBarContainer, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, borderWidth, borderColor, dashed, transformObject, titleBarWidth, titleBarHeight, titleBarX, titleBarY, titleBarDraggable, close, closeColor, closeBackgroundColor, closeIndicatorColor, anchor, style, group, inherit) {
		var sig = "width, height, titleBar, titleBarContainer, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, borderWidth, borderColor, dashed, transformObject, titleBarWidth, titleBarHeight, titleBarX, titleBarY, titleBarDraggable, close, closeColor, closeBackgroundColor, closeIndicatorColor, anchor, style, group, inherit";
		var duo; if (duo = zob(zim.Layer, arguments, sig, this)) return duo;
		z_d("58.5");

		this.group = group;
		var DS = style===false?{}:zim.getStyle("Layer", this.group, inherit);

		if (zot(width)) width=DS.width!=null?DS.width:500;
		if (zot(height)) height=DS.height!=null?DS.height:500;

		this.zimContainer_constructor(0,0,width,height,false);
		this.type = "Layer";

		var that = this;
		that.distX = 40; // titleBar distance from top left corner of Container
		that.distY = 0;
		if (zot(titleBar)) titleBar=DS.titleBar!=null?DS.titleBar:"LAYER";
		var titleBarOriginal = titleBar;
		var titleBarText = titleBar;
		var titleBarDefault = true;

		if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"#eee";
		if (zot(rollBackgroundColor)) rollBackgroundColor=DS.rollBackgroundColor!=null?DS.rollBackgroundColor:"#fff";
		if (zot(selectedBackgroundColor)) selectedBackgroundColor=DS.selectedBackgroundColor!=null?DS.selectedBackgroundColor:"#666";
		if (zot(color)) color=DS.color!=null?DS.color:"#666";
		if (zot(rollColor)) rollColor=DS.rollColor!=null?DS.rollColor:"#666";
		if (zot(selectedColor)) selectedColor=DS.selectedColor!=null?DS.selectedColor:"#ddd";
		if (zot(borderWidth)) borderWidth=DS.borderWidth!=null?DS.borderWidth:1;
		if (zot(borderColor)) borderColor=DS.borderColor!=null?DS.borderColor:backgroundColor;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		if (zot(dashed)) dashed=DS.dashed!=null?DS.dashed:true;
		if (zot(transformObject)) transformObject=DS.titleBar!=null?DS.titleBar:null;
		if (zot(titleBarWidth)) titleBarWidth=DS.titleBarWidth!=null?DS.titleBarWidth:100;
		var originalTitleBarWidth = titleBarWidth;
		if (zot(titleBarHeight)) titleBarHeight=DS.titleBarHeight!=null?DS.titleBarHeight:40;
		if (zot(titleBarX)) titleBarX=DS.titleBarX!=null?DS.titleBarX:null;
		if (zot(titleBarY)) titleBarY=DS.titleBarY!=null?DS.titleBarY:null;
		if (zot(titleBarDraggable)) titleBarDraggable=DS.titleBarDraggable!=null?DS.titleBarDraggable:true;
		if (zot(close)) close=DS.close!=null?DS.close:true;
		if (zot(closeColor)) closeColor=DS.closeColor!=null?DS.closeColor:selectedBackgroundColor;
		if (zot(closeBackgroundColor)) closeBackgroundColor=DS.closeBackgroundColor!=null?DS.closeBackgroundColor:selectedBackgroundColor;
		if (zot(closeIndicatorColor)) closeIndicatorColor=DS.closeIndicatorColor!=null?DS.closeIndicatorColor:selectedColor;
		if (zot(anchor)) anchor=DS.anchor!=null?DS.anchor:true;
		if (!titleBarDraggable) anchor = false;
		that.anchor = anchor;
		if (close) titleBarWidth += 30;

		transformObject = zim.merge({borderColor:selectedBackgroundColor}, transformObject, {events:true, visible:false, ghostColor:borderColor, ghostWidth:borderWidth, ghostDashed:dashed, ghostHidden:true});

		// do the defaultFrame thing for the container
		if (zot(titleBarContainer)) titleBarContainer=DS.titleBarContainer!=null?DS.titleBarContainer:null;
		if (zot(titleBarContainer)) {
			if (zimDefaultFrame) {
				titleBarContainer = zimDefaultFrame.stage;
			} else {
				zog("zim Layer(): Please pass in a reference to a container with bounds set.");
				return;
			}
		}

		var downCheck = false;
		that.active = false;
		that.turnOff = function(noHide, noShow) {
			that.active = false;
			if (close) that.checkBox.visible = false;
			that.transformControls.remove();
			that.mouseChildren = true;
			that.button.backgroundColor = backgroundColor;
			that.button.rollBackgroundColor = rollBackgroundColor;
			that.button.color = color;
			that.button.rollColor = color;
			that.transformControls.allowToggleOff();
			if (that.resizeChildren) that.resizeChildren();
		}
		that.turnOn = function() {
			that.active = true;
			if (close) {
				that.checkBox.visible = true;
				that.checkBox.checked = true;
			}
			that.button.backgroundColor = selectedBackgroundColor;
			that.button.rollBackgroundColor = selectedBackgroundColor;
			that.button.color = selectedColor;
			that.button.rollColor = selectedColor;
			that.mouseChildren = false;
			that.transformControls.add();
			that.transformControls.allowToggleOn();
		}
		var stage;
		that.titleBarPos = function(x, y) {
			if (that.titleBar.pos) {
				that.titleBar.pos(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]); // ES6 wish
				stage.update();
			} else {
				that.distX = x;
				that.distY = y;
			}
			titleBarDefault = false;
			return that;
		}
		var lastVisible = that.visible;
		that.visible = false;
		var firstControl;
		this.added(function (theStage) {
			that.transform(transformObject); // in case persist is set do this first and wait a little bit with visible false
			if (borderWidth >= 0) that.transformControls.hideGhost();
			zim.timeout(50, function () {
				function firstPoint() {return that.localToLocal(0,0,titleBarContainer);}
				that.visible = lastVisible;
				if (borderWidth >= 0) {
					zim.timeout(100, function(){
						that.transformControls.showGhost();
						stage.update();
					});
				}
				stage = theStage;
				var titleBar = that.titleBar = new zim.Container(titleBarWidth, titleBarHeight)
					.reg(0,titleBarHeight)
					.loc(that, null, titleBarContainer)
					.mov(that.distX,that.distY);

				if (titleBarDraggable) titleBar.drag({all:true, boundary:new zim.Boundary(0,titleBarHeight,titleBarContainer.width-titleBarWidth,titleBarContainer.height-titleBarHeight), localBounds:true});
				if (that.distX != 40 || that.distY !=0) {
					titleBar.pos(that.distX, that.distY);
				}
				if (close) {
					that.checkBox = new zim.CheckBox({
						borderColor:closeColor,
						backgroundColor:closeBackgroundColor,
						indicatorColor:closeIndicatorColor,
						size:20, startChecked:true
					}).center(titleBar).pos(0, null, true).change(function () {
						that.turnOff(true, true);
					});
					that.checkBox.visible = false;
				}
				titleBar.mouseChildren = true;
				titleBar.layer = that;

				that.resetTitleBar = function() {
					that.distX = 40;
					that.distY = 0;
					titleBarDefault = true;
					that.move(true);
					stage.update();
				}

				if (typeof titleBarText === "string" || typeof titleBarText === "number") titleBarText = new zim.Label({
					text:titleBarText, color:color, rollColor:rollColor, size:DS.size!=null?DS.size:18,
					group:this.group
				});
				that.label = titleBarText;
				that.label.center(titleBar);
				that.button = new zim.Button({
					shadowBlur:-1,
					width:originalTitleBarWidth, height:titleBarHeight-1, label:that.label,
					color:color,
					rollColor:rollColor,
					backgroundColor:backgroundColor,
					rollBackgroundColor:rollBackgroundColor,
					corner:(DS.corner!=null?DS.corner:0),
					inherit:DS
				}).addTo(titleBar);
				that.button.on("mousedown", function () {
					downCheck = true;
					// all other layers get turned off
					titleBarContainer.loop(function (bar) {
						if (bar && bar != titleBar && bar.layer && bar.layer.turnOff && bar.layer.active) bar.layer.turnOff(true, true);
					});
					if (!that.active) that.turnOn();
					titleBar.top();
				});
				that.button.on("pressmove", function(){matchLocation()});
				that.button.on("pressup", function () {
					matchLocation(true);
					var point = that.localToLocal(0, 0, titleBarContainer);
					that.distX = titleBar.x-point.x;
					that.distY = titleBar.y-point.y;
					titleBar.top();
					downCheck = false;
					if (!edgeCheck() && titleBarDraggable) {
						var point = that.localToGlobal(0,0);
						if (that.button.hitTestPoint(point.x, point.y)) that.resetTitleBar();
					}
				});
				function matchLocation(dispatch) {
					if (titleBarDefault && titleBarDraggable) {
						var desiredTopLeft = titleBarContainer.localToLocal(titleBar.x-that.distX, titleBar.y-that.distY, that.parent);
						var regPoint = that.localToLocal(that.regX, that.regY, that.parent);
						var topLeft = that.localToLocal(0,0,that.parent);
						that.x =  desiredTopLeft.x + (regPoint.x-topLeft.x);
						that.y =  desiredTopLeft.y + (regPoint.y-topLeft.y);
						that.transformControls.resize(dispatch); // TransformManager saves all objects any time a single object changes
						recursiveLocation(that);
					}
				}
				function recursiveLocation(obj) {
					obj.loop(function (o) {
						if (o.type == "Layer") {
							if (that.titleBarDraggable) o.move();
							if (o.transformControls.ghost) o.transformControls.resize(); // to move ghost
							recursiveLocation(o);
						}
					});
				}
				that.button.on("dblclick", function () {
					if (titleBarDraggable) that.resetTitleBar();
				});

				that.turnOff();
				that.on("transformed", function (e) {
					if (e.transformType=="move" || e.transformType=="size" || e.transformType=="stretch" || e.transformType=="rotate") {
						if (titleBarDraggable) that.move();
						recursiveLocation(that);
					}
				});
				function edgeCheck() {
					if (titleBar.x == 0) return true;
					if (titleBar.y == titleBar.height) return true;
					if (titleBar.x == titleBarContainer.width-titleBar.width) return true;
					if (titleBar.y == titleBarContainer.height) return true;
				}
				that.move = function(override) {
					if (!override && !titleBarDefault && that.anchor) {
						if (edgeCheck()) return;
					}
					titleBar.loc(firstPoint()).mov(that.distX, that.distY);
					if (titleBar.x < 0) {titleBarDefault=false; titleBar.x = 0;}
					if (titleBar.y < titleBar.height) {titleBarDefault=false; titleBar.y = titleBar.height;}
					if (titleBar.x > titleBarContainer.width-titleBar.width) {titleBarDefault=false; titleBar.x = titleBarContainer.width-titleBar.width;}
					if (titleBar.y > titleBarContainer.height) {titleBarDefault=false; titleBar.y =titleBarContainer.height}
					if (!that.anchor) titleBarDefault = true;
				}
				that.on("transformhide", function() {
					that.turnOff(true, true);
					titleBarContainer.loop(function (bar) {
						if (bar && bar != titleBar && bar.layer && bar.layer.turnOff && bar.layer.active) bar.layer.turnOff();
					});
				});
				that.resizeChildren = function(dispatch) {
					that.transformControls.resize(dispatch);
					// if (that.transformControls.visible) that.transformControls.hide();
					that.loop(function (obj) {
						if (obj.transformControls) {
							if (obj.transformControls.visible) obj.transformControls.hide();
							if (obj.type == "Layer") {
								obj.resizeChildren();
							} else {
								obj.transformControls.resize();
							}
						}
					});
				}
				that.resize = function(dispatch) {
                    that.move();
                    that.resizeChildren(dispatch);
					return that;
                }
				that.toggled = false;
				function setToggled() {
					if (that.toggled) {
						titleBarContainer.loop(function (bar) {
							if (bar && bar != titleBar && bar.layer && bar.layer.turnOff && bar.layer.active) bar.layer.turnOff();
						});
						that.turnOn();
					} else {
						titleBar.top();
						that.turnOff();
						titleBarContainer.loop(function (bar) {
							if (bar && bar != titleBar && bar.layer && bar.layer.turnOff && bar.layer.active) bar.layer.turnOff();
						});
					}
					if (that.stage) that.stage.update();
				}
				that.toggle = function(state) {
					if (zot(state)) {
						that.toggled = !that.toggled;
					} else {
						that.toggled = state;
					}
					setToggled();
					return that;
				}
				Object.defineProperty(that, 'titleBarDraggable', {
					get: function() {
						return titleBarDraggable;
					},
					set: function(value) {
		                titleBarDraggable = value;
						if (titleBarDraggable) titleBar.drag({all:true, boundary:zim.Boundary(0,40,titleBarContainer.width-titleBarWidth,titleBarContainer.height-titleBarHeight), localBounds:true})
						else titleBar.noDrag();
					}
				});
				stage.on("stagemouseup", function () {
					if (that.transformControls.visible) titleBar.top();
				});
				if (!zot(titleBarX) || !zot(titleBarY)) {
					titleBarDefault = false;
					var x = !zot(titleBarX)?titleBarX:titleBar.x;
					var y = !zot(titleBarY)?titleBarY:titleBar.y-titleBarHeight;
					that.titleBarStartX = titleBarX;
					that.titleBarStartY = titleBarY;
					that.titleBarPos(x,y);
					var point = that.parent.localToLocal(that.x, that.y, titleBarContainer);
					that.distX = titleBar.x-point.x;
					that.distY = titleBar.y-point.y;
				}
			}); // end timeout
		}); // end added

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.Layer(width, height, titleBarOriginal, titleBarContainer, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, borderWidth, borderColor, dashed, transformObject, originalTitleBarWidth, titleBarHeight, titleBarX, titleBarY, titleBarDraggable, close, closeColor, closeBackgroundColor, closeIndicatorColor, anchor, style, this.group, inherit));
		}

		this.dispose = function() {
			that.transformControls.remove();
			that.transformControls.disable();
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.Layer, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-58.5

/*--
zim.Waiter = function(container, speed, foregroundColor, backgroundColor, corner, shadowColor, shadowBlur, fadeTime, style, group, inherit)

Waiter
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Adds a little animated three dot wait widget.
You need to call waiter.show() to show the waiter and waiter.hide() to hide it.
You do not need to add it to the stage - it adds itself centered.
You can change the x and y (with origin and registration point in middle).

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var waiter = new Waiter(stage);
waiter.show(); // show the waiter until assets load
frame.loadAssets("greeting.mp3");
frame.on("complete", function() {
	waiter.hide();
	frame.asset("greeting.mp3").play();
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
container - (default first frame's stage) the container that holds the waiter
speed - (default 600) cycle time in milliseconds
backgroundColor - (default "orange") the background color
foregroundColor - (default "white") the dot color
corner - (default 14) the corner radius of the waiter box
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
shadowColor - (defaults rgba(0,0,0,.3)) set to -1 for no shadow
shadowBlur - (default 14) the blur of the shadow if shadow is set
fadeTime - (default 0) milliseconds to fade in and out
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
show() - shows the waiter (returns the waiter for chaining)
hide() - hides the waiter
toggle(state - default null) - shows waiter if hidden and hides waiter if showing (returns the object for chaining)
	or pass in true to show waiter or false to hide waiter
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied (returns the new waiter for chaining)
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
display - reference to the waiter backing graphic
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+59
	zim.Waiter = function(container, speed, foregroundColor, backgroundColor, corner, shadowColor, shadowBlur, fadeTime, style, group, inherit) {
		var sig = "container, speed, foregroundColor, backgroundColor, corner, shadowColor, shadowBlur, fadeTime, style, group, inherit";
		var duo; if (duo = zob(zim.Waiter, arguments, sig, this)) return duo;
		z_d("59");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Waiter";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(speed)) speed=DS.speed!=null?DS.speed:600; // ms cycle time
		if (zot(foregroundColor)) foregroundColor=DS.foregroundColor!=null?DS.foregroundColor:"white";
		if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"orange";
		if (zot(corner)) corner=DS.corner!=null?DS.corner:16;
		if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:14;
		if (zot(fadeTime)) fadeTime=DS.fadeTime!=null?DS.fadeTime:0;

		var height = 40;
		var numDots = 3;
		var r = height*.6/2;
		var s = (height-r*2)/2;
		var width = numDots*(r*2+s)+s;

		this.setBounds(-width/2,-height/2, width, height);

		var that = this;

		var display = this.display = new zim.Shape({style:false});
		this.addChild(display);
		display.setBounds(0, 0, width, height);
		display.regX = width/2;
		display.regY = height/2;
		var g = display.graphics;
		g.f(backgroundColor);
		var cc = corner;
		if (!Array.isArray(cc)) cc = [corner, corner, corner, corner];
		g.rc(0, 0, width, height, cc[0], cc[1], cc[2], cc[3]);
		if (shadowColor != -1 && shadowBlur > 0) display.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
		display.on("click", function(e) {
			// stops the click from going through the display to the background
			e.stopImmediatePropagation();
		});

		var circles = new zim.Container({style:false});
		this.addChild(circles);

		var dot;
		for (var i=0; i<numDots; i++) {
			dot = new createjs.Shape();
			dot.graphics.f(foregroundColor).dc(0,0,r);
			dot.x = (i-(numDots-1)/2) * (r*2+s);
			circles.addChild(dot);
			dot.cache(-r,-r,r*2,r*2);
			dot.alpha = 0;
		}

		this.hide = function() {
			if (fadeTime > 0) {
				that.animate({obj:{alpha:0}, time:fadeTime, call:end});
			} else {
				end();
			}
			function end() {
				if (that.parent) that.parent.removeChild(that);
				var stage = that.stage;
				if (stage) stage.update();
				if (that.zimAccessibility) {
					var a = that.zimAccessibility;
					a.resize(that);
					if (accessibilityClicker) accessibilityClicker.focus();
					else that.zimTabTag.nextSibling.focus();
					setTimeout(function() {a.talk("Waiter has finished.");}, 50);
				}
			}
			that.toggled = false;
			return that;
		}
		var accessibilityClicker;
		var timeouts = [];
		this.show = function() {
			var mess = "zim display - Waiter(): Please pass in a reference to a container with bounds set as first parameter to Waiter";
			if (zot(container)) {
				if (zimDefaultFrame) {
					container = zimDefaultFrame.stage;
				} else {
					zog(mess);
					return;
				}
			} else if (!container.getBounds) {
				zog(mess);
				return;
			} else if (zot(container.stage)) {
				zog("zim display - Waiter(): The container must have a stage property");
				return;
			}
			var dot; var counter=0;
			for (var i=0; i<circles.numChildren; i++) {
				if (circles) {
					timeouts.push(setTimeout(function() {
						dot = circles.getChildAt(counter);
						createjs.Tween.get(dot,{loop:true})
							.to({alpha:1}, speed/numDots/2)
							.wait(speed/numDots)
							.to({alpha:0}, speed/numDots)
							.wait(speed-speed/numDots-speed/numDots/2);
						counter++;
					}, i*speed/numDots));
				}
			}
			that.ticker = createjs.Ticker.on("tick", function() {container.stage.update();});

			that.x = (container.getBounds().width) /2;
			that.y = (container.getBounds().height) /2;
			container.addChild(that);
			if (fadeTime > 0) {
				that.alpha = 0;
				that.animate({alpha:1}, fadeTime);
			}
			if (that.zimAccessibility) {
				var a = that.zimAccessibility;
				setTimeout(function(){if (a.activatedObject) accessibilityClicker = a.activatedObject.zimTabTag;}, 50);
				a.resize(that);
				a.talk(that.zimTabTag.getAttribute("aria-label"));
			}
			that.toggled = true;
			return that;
		}
		that.toggle = function(state) {
			if (state===true) that.show();
			else if (state===false) that.hide();
			else if (that.parent) that.hide();
			else that.show();
			return that;
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.Waiter(container, speed, foregroundColor, backgroundColor, corner, shadowColor, shadowBlur, fadeTime, style, this.group, inherit));
		}

		this.dispose = function() {
			if (that.ticker) createjs.Ticker.off("tick", that.ticker);
			createjs.Tween.removeTweens(that);
			that.removeFrom();
			for (var i=0; i<circles.numChildren; i++) {
				var circle = circles.getChildAt(i);
				clearInterval(timeouts[i]);
				createjs.Tween.removeTweens(circle);
			}
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.Waiter, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-59

/*--
zim.ProgressBar = function(barType, foregroundColor, backgroundColor, borderColor, borderWidth, padding, label, color, labelPosition, percentage, corner, shadowColor, shadowBlur, backing, delay, fastClose, container, autoHide, style, group, inherit)

ProgressBar
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Adds a little progress bar that can be scaled if desired
Pass in to progress parameter of the Frame or LoadAssets call to operate
or use on its own with the show(), hide() methods and percent property

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var progressBar = new ProgressBar();
frame.loadAssets({assets:"greeting.mp3", progress:progressBar});
// a bar will be centered on the default stage (or specify a container)
// the bar will show a percentage of asset bytes loaded
frame.on("complete", function() {
	// the bar will be removed when loading is complete
	frame.asset("greeting.mp3").play();
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
barType - (default "circle") set "rectangle" for rectangular progress bar
foregroundColor - (default green) the color of the bar
backgroundColor - (default dark) the color of the background
borderColor - (default backgroundColor) the color of the border - for "rectangle" barType
borderWidth - (default 10 "circle" or 0 "rectangle") the width of the border (or ring for "circle" barType)
padding - (default 2 for "circle" or 0 for "rectangle") the space between the bar and the backing
label - (null) set to a String or a ZIM Label to specifify label properties
color - (default foregroundColor) the color of the label if there is one
labelPosition - ("bottom" if label specified) also set to "top" to move label above progress bar
percentage - (default false) set to true to show percentage after label (set label to "" for only percentage)
corner - (default 15 for "rectangle" barType) set to 0 for square corners, etc.
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
shadowColor - (default rgba(0,0,0,.3)) set to -1 for no shadow
shadowBlur - (default 14) the blur of the shadow if shadow is set
backing - (default null) a Display object for the backing of the "rectangle" barType (eg. Shape, Bitmap, Container, Sprite)
    See ZIM Pizzazz 3 - for patterns - these have a type of "Pattern" which makes them special
    If a "pattern" is used then the normal backing will be used to mask the pattern
delay - (default 100) milliseconds to delay view of progress bar - helps not flash progress bar when content is cached
fastClose - (default true) hide as soon as progress is done
	The complete event is delayed slightly after the progress bar is loaded
	Set to false to wait until the complete event triggers before removing the progress bar
container - (defaultFrame's stage) or specify a container to hold the progress bar
autoHide - (default true) set to false so bar does not hide when reaching 100%
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
show() - shows the progress bar (returns the progress bar for chaining)
hide() - hides the progress bar
toggle(state - default null) - shows progress bar if hidden and hides progress bar if showing (returns the object for chaining)
	or pass in true to show progress bar or false to hide progress bar
setBacking(backing) - change the backing of the progress bar
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied (returns the new waiter for chaining)
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
percent - get or set the percent (0-100) complete of the progress bar
label - reference to the label if there is one
backing - reference to the backing shape.  This may be the backing DisplayObject if provided
	the backing will have a pattern property if a pattern is set for the backing
bar - reference to the bar shape
toggled - read-only Boolean that is true if progress bar is showing otherwise false
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
display - reference to the waiter backing graphic
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.
group - used when the object is made to add STYLE with the group selector (like a CSS class)

EVENTS
See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+59.5
	zim.ProgressBar = function(barType, foregroundColor, backgroundColor, borderColor, borderWidth, padding, label, color, labelPosition, percentage, corner, shadowColor, shadowBlur, backing, delay, fastClose, container, autoHide, style, group, inherit) {
		var sig = "barType, foregroundColor, backgroundColor, borderColor, borderWidth, padding, label, color, labelPosition, percentage, corner, shadowColor, shadowBlur, backing, delay, fastClose, container, autoHide, style, group, inherit";
		var duo; if (duo = zob(zim.ProgressBar, arguments, sig, this)) return duo;
		z_d("59.5");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "ProgressBar";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(foregroundColor)) foregroundColor=DS.foregroundColor!=null?DS.foregroundColor:"#acd241";
		if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"#444";
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:backgroundColor;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (zot(labelPosition)) labelPosition=DS.labelPosition!=null?DS.labelPosition:"bottom";
		if (zot(barType)) barType=DS.barType!=null?DS.barType:"circle";
		if (zot(padding)) padding=DS.padding!=null?DS.padding:(barType=="circle"?2:-.5);
		if (zot(corner)) corner=DS.corner!=null?DS.corner:15;
		if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:14;
		if (zot(backing)) backing=DS.backing!=null?DS.backing.clone():null;
		if (zot(fastClose)) fastClose=DS.fastClose!=null?DS.fastClose:true;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) {
			if (barType == "circle") borderWidth = 10;
			else borderWidth = 2;
		}
		if (zot(delay)) delay= DS.delay!=null?DS.delay:100;
		if (zot(autoHide)) autoHide= DS.autoHide!=null?DS.autoHide:true;
		if (zot(label)) label = DS.label!=null?DS.label:null;
		if (zot(color)) color=DS.color!=null?DS.color:foregroundColor;

		if (typeof label === "string" || typeof label === "number") label = new zim.Label({
			text:label, color:color,
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
			group:this.group
		});
		var emptyLabel = false;
		if (label && label.text == "") emptyLabel = true;
		if (!zot(percentage) && zot(label)) label = new zim.Label("");
		this.label = label;

		var height = 30;
		var that = this;
		var _percent = 0;

		that.visible = false;
		setTimeout(function(){that.visible = true; if (that.stage) that.stage.update();}, delay);

		if (barType == "circle") {
			var width = 20;
			var backing = this.backing = new zim.Circle(width, "rgba(0,0,0,0)", backgroundColor, borderWidth, null, null, null, false).addTo(this);
			var bar = this.bar = new zim.Shape({style:false}).addTo(that).pos({x:backing.x, y:backing.y, reg:true}).rot(-90);
		} else {
			var width = 200;
			if (shadowColor != -1 && shadowBlur > 0) {
				var shadowRect = new zim.Rectangle(width-2, height-2, backgroundColor, null, null, corner, null, null, false).addTo(this);
				shadowRect.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
			}
			var backing = doBacking(backing);

			var cc = corner;
			if (!Array.isArray(cc)) cc = [corner, corner, corner, corner];
			var cAdjust = (padding+borderWidth/2)*1.2;
			var bar = this.bar = new zim.Rectangle(width-(padding+borderWidth/2)*1.8, height-(padding+borderWidth/2)*1.8, foregroundColor, null, null, [cc[0]-cAdjust,cc[1]-cAdjust,cc[2]-cAdjust,cc[3]-cAdjust], null, null, false).center(this);
			var mask = this.mask = new zim.Rectangle(width-(padding+borderWidth/2)*1.8, height-(padding+borderWidth/2)*1.8, null, null, null, null, null, null, false).center(this).alp(0).sca(0,1);
			bar.setMask(mask);
		}

		function doBacking(newBacking) {
			if (that.backing) {
				var index = that.getChildIndex(that.backing);
				that.removeChild(that.backing);
			} else {
				var index = that.numChildren; // may or may not be shadow rect
			}
			if (that.border) that.removeChild(that.border);
			if (!zot(newBacking) && newBacking.type == "Pattern") {
				var pattern = newBacking;
				var newBacking = that.backing = new zim.Rectangle(width, height, backgroundColor, null, null, corner, null, null, false).addTo(that, index);
				newBacking.pattern = pattern;
				pattern.center(newBacking);
				var bounds = newBacking.getBounds();
				pattern.setMask(newBacking.shape);
				if (borderWidth) var border = that.border = new zim.Rectangle(width, height, "rgba(0,0,0,0)", borderColor, borderWidth, corner, null, null, false).addTo(that, index+1);
			} else {
				var newBacking = that.backing = zot(newBacking) ? new zim.Rectangle(width, height, backgroundColor, borderColor, borderWidth, corner, null, null, false).addTo(that, index) : backing.addTo(that, index);
			}
			return newBacking;
		}

		that.setBacking = function(backing) {
			doBacking(backing);
			if (that.stage) that.stage.update();
		}

		backing.on("click", function(e) {
			// stops the click from going through the display to the background
			e.stopImmediatePropagation();
		});

		if (!zot(label)) {
			label.scaleX = label.scaleY = .8;
			label.startText = label.text;
			if (!zot(percentage)) label.text = label.startText + "  0%";
			label.center(that);
			if (labelPosition == "above") {
				label.y -= 60;
			} else {
				label.y += 60;
			}
			label.alpha = .8;
		}

		this.hide = function() {
			var stage = that.stage;
			if (that.parent) that.parent.removeChild(that);
			if (that.backing.type == "Pattern" && that.backing.pauseInterval) that.backing.pauseInterval();
			if (stage) stage.update();
			if (that.zimAccessibility) {
				var a = that.zimAccessibility;
				a.resize(that);
				if (accessibilityClicker) accessibilityClicker.focus();
				else that.zimTabTag.nextSibling.focus();
				setTimeout(function() {a.talk("Progress Bar has finished.");}, 50);
			}
			that.toggled = false;
			return that;
		}
		var accessibilityClicker;
		var waiterTimeout;
		this.show = function() {
			var mess = "zim display - ProgressBar(): Please pass in a reference to a container with bounds set as first parameter of the ProgressBar";
			if (zot(container)) {
				if (zimDefaultFrame) {
					container = zimDefaultFrame.stage;
				} else {
					zog(mess);
					return;
				}
			} else if (!container.getBounds) {
				zog(mess);
				return;
			} else if (zot(container.stage)) {
				zog("zim display - Waiter(): The container must have a stage property");
				return;
			}
			if (that.timeOut) clearTimeout(that.timeOut);
			changePercent(0);
			if (that.zimActiveLoader) {
				that.zimActiveLoader.on("progress", function(e) {
					_percent = e.progress*100;
					changePercent(_percent);
				});
			}
			that.center(container);

			if (that.backing.type == "Pattern" && that.backing.pauseInterval) that.backing.pauseInterval(false);

			if (that.zimAccessibility) {
				var a = that.zimAccessibility;
				setTimeout(function(){if (a.activatedObject) accessibilityClicker = a.activatedObject.zimTabTag;}, 50);
				a.resize(that);
				a.talk(that.zimTabTag.getAttribute("aria-label"));
			}
			that.toggled = true;
			return that;
		}
		that.toggle = function(state) {
			if (state===true) that.show();
			else if (state===false) that.hide();
			else if (that.parent) that.hide();
			else that.show();
			return that;
		}

		function changePercent(amount) {
			if (barType == "circle") {
				bar.graphics
					.c()
					.mt(0, 0)
					.s(foregroundColor)
					.ss(borderWidth-padding*2+.5)
					.a(0, 0, width, 0, 360*amount/100*Math.PI/180);
			} else {
				mask.sca(amount/100, 1);
				bar.setMask(mask);
			}
			if (!zot(percentage)) label.text = label.startText + " " + Math.min(Math.round(amount), 100) + "%";
			if (autoHide && fastClose && Math.round(amount) >= 100) that.timeOut = setTimeout(function(){that.hide();}, 200);
			if (that.stage) that.stage.update();
		}

		Object.defineProperty(that, 'percent', {
			get: function() {
				return _percent;
			},
			set: function(value) {
				_percent = value
				changePercent(value);
			}
		});

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.ProgressBar(barType, foregroundColor, backgroundColor, borderColor, borderWidth, padding, label, color, labelPosition, percentage, corner, shadowColor, shadowBlur, backing, delay, fastClose, container, autoHide, style, this.group, inherit));
		}

		this.dispose = function() {
			if (that.backing.type == "Pattern" && that.backing.clearInterval) that.backing.clearInterval();
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.ProgressBar, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-59.5

/*--
zim.Indicator = function(width, height, num, foregroundColor, backgroundColor, borderColor, borderWidth, backdropColor, corner, indicatorType, fill, scale, lightScale, press, shadowColor, shadowBlur, style, group, inherit)

Indicator
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A row of dots or squares that can be used to indicate a step, page, level, score, etc.
The indicator can be used as an input as well but often these are small so may not be best to rely on.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var lights = new Indicator({fill:true});
lights.selectedIndex = 0; // set the first light on
lights.center(stage);
stage.on("stagemousedown", function() {
	// increase the indicator lights each click (then start over)
	lights.selectedIndex = (lights.selectedIndex+1) % lights.num;
});
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 100) width of indicator
height - (default 50) height of indicator
num - (default 6) the number of lights
foregroundColor - (default "orange") color of the light(s) turned on
backgroundColor - (default "grey") color of the light(s) turned off
borderColor - (default -1 for no border) border color of lights and backdrop (if backdrop)
borderWidth - (default 1 if stroke is set) the size of the stroke in pixels
backdropColor - (default -1 for no backdrop) backdrop rectangle around lights
corner - (default 0) the corner radius if there is a backdropColor provided
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
indicatorType - (default "dot" or "circle") can also be "box" or "square"
fill - (default false) set to true to fill in lights to the left of the selectedIndex
scale - (default 1) for all the lights including spacing
lightScale - (default 1) scale for each light - keeping the spacing unchanged
press - (default false) set to true to make lights clickable
shadowColor - (default rgba(0,0,0,.3)) set to -1 for no shadow
shadowBlur - (default 5) the shadow blur if shadow is set
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
selectedIndex - gets or sets the current index of the indicator
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
num - the assigned num value (how many light objects) (read only)
backdrop - gives access to the backdrop if there is one Rectangle
lights - an array of the light objects (zim Circle or Rectangle objects)
lightsContainer - gives access to the lights createjs.Container with its Circle or Rectangle children
enabled - set to false to disable component
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "change" event if press is true and indicator is pressed on and lights change

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+60
	zim.Indicator = function(width, height, num, foregroundColor, backgroundColor, borderColor, borderWidth, backdropColor, corner, indicatorType, fill, scale, lightScale, press, shadowColor, shadowBlur, style, group, inherit) {
		var sig = "width, height, num, foregroundColor, backgroundColor, borderColor, borderWidth, backdropColor, corner, indicatorType, fill, scale, lightScale, press, shadowColor, shadowBlur, style, group, inherit";
		var duo; if (duo = zob(zim.Indicator, arguments, sig, this)) return duo;
		z_d("60");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Indicator";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(width)) width = DS.width!=null?DS.width:300;
		if (zot(height)) height = DS.height!=null?DS.height:50;
		if (zot(num)) num = DS.num!=null?DS.num:6;
		if (zot(foregroundColor)) foregroundColor = DS.foregroundColor!=null?DS.foregroundColor:"#f58e25";
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"#666";
		if (backgroundColor < 0) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"rgba(0,0,0,.01)";
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:null;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(backdropColor)) backdropColor = DS.backdropColor!=null?DS.backdropColor:-1;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:0;
		if (zot(indicatorType)) indicatorType = DS.indicatorType!=null?DS.indicatorType:"dot";
		if (zot(fill)) fill = DS.fill!=null?DS.fill:false;
		if (zot(scale)) scale = DS.scale!=null?DS.scale:1;
		if (zot(lightScale)) lightScale = DS.lightScale!=null?DS.lightScale:1;
		if (zot(press)) press = DS.press!=null?DS.press:false;
		if (zot(shadowColor)) shadowColor = DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur = DS.shadowBlur!=null?DS.shadowBlur:5;

		var eventType = (!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click";

		var that = this;
		this.lights = [];

		var myValue;
		var indicator = new zim.Container({style:false});
		if (backdropColor != -1) {
			var backing = that.backdrop = new zim.Rectangle(width, height, backdropColor, borderColor, borderWidth, corner, null, null, false);
			this.addChild(backing);
		}
		var lights = this.lightsContainer = new zim.Container({style:false});
		this.addChild(lights);
		var light;
		var size = height * .5;
		var space = width / (num+1);
		var hitArea = new createjs.Shape();
		if (indicatorType == "square" || indicatorType == "box") {
			hitArea.graphics.f("black").dr(-space/2/lightScale+size/2, -height/2+size/2, space/lightScale, height);
		} else {
			hitArea.graphics.f("black").dr(-space/2/lightScale, -height/2, space/lightScale, height);
		}
		for (var i=0; i<num; i++) {
			if (indicatorType == "square" || indicatorType == "box") {
				light = new zim.Rectangle(size, size, backgroundColor, borderColor, borderWidth, null, null, null, false);
				light.regX = light.width/2;
				light.regY = light.height/2;
			} else {
				light = new zim.Circle(size/2, backgroundColor, borderColor, borderWidth, null, null, null, false);
			}
			this.lights.push(light);
			light.znum = i;
			light.scaleX = light.scaleY = lightScale;
			light.hitArea = hitArea;
			light.x = space + space * i;
			light.y = height / 2;
			lights.addChild(light);
		}
		lights.setBounds(0,0,width,height);
		lights.regX = lights.x = width / 2;
		lights.regY = lights.y = height / 2;
		this.addChild(lights);
		if (shadowColor != -1 && shadowBlur > 0) lights.shadow = new createjs.Shadow(shadowColor, 2, 2, shadowBlur);

		if (press) {
			lights.cursor = "pointer";
			var lightsEvent = lights.on(eventType, function(e) {
				if (myValue == e.target.znum) return;
				myValue = e.target.znum;
				setLights(myValue);
				that.dispatchEvent("change");
			});
		}
		lights.scaleX = lights.scaleY = scale;

		function setLights(v) {
			if (v >= num) v = -1; // out of range - don't let it fill up
			var c;
			for (var i=0; i<num; i++) {
				if (fill) {
					if (i < v) c = foregroundColor;
					else c = backgroundColor;
				} else {
					c = backgroundColor;
				}
				if (i == v) c = foregroundColor;
				lights.getChildAt(i).color = c;
			}
			if (that.zimAccessibility) that.zimAccessibility.changeTitle(that);

			if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
		}

		Object.defineProperty(this, 'selectedIndex', {
			get: function() {
				return myValue;
			},
			set: function(value) {
				myValue = Math.floor(value);
				myValue = zim.constrain(myValue, -1, num-1);
				setLights(myValue);
			}
		});

		Object.defineProperty(this, 'num', {
			get: function() {
				return num;
			},
			set: function(value) {
				if (zon) zog("num is read only");
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
			}
		});

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.Indicator(width, height, num, foregroundColor, backgroundColor, borderColor, borderWidth, backdropColor, corner, indicatorType, fill, scale, lightScale, press, shadowColor, shadowBlur, style, this.group, inherit));
		}
	}
	zim.extend(zim.Indicator, zim.Container, "clone", "zimContainer", false);
	//-60

/*--
zim.List = function(width, height, list, viewNum, vertical, currentSelected, align, valign, labelAlign, labelValign, labelIndent, labelIndentHorizontal, labelIndentVertical, indent, spacing, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, backdropColor, color, selectedColor, rollColor, selectedRollColor, borderColor, borderWidth, padding, corner, swipe, scrollBarActive, scrollBarDrag, scrollBarColor, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, slide, slideDamp, slideSnap, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, scrollWheel, damp, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, excludeCustomTap, organizer, checkBox, clone, cancelCurrentDrag, style, group, inherit)

List
zim class - extends a zim.Window which extends a zim.Container which extends a createjs.Container

DESCRIPTION
A vertical or horizontal list of items.
This is really a zim.Tabs object inside a zim.Window object.
The list can be strings, numbers or Label objects and these are added to Tabs buttons.
The list can also include any DisplayObject with bounds (which most ZIM objects have except a Shape needs bounds set manually with setBounds()).
If the object is not a string, number or Label then selection will not highlight and currently animateTo() may not work if size is different.
See: https://zimjs.com/explore/list.html
See: https://zimjs.com/explore/listObjects.html

ACCORDION
An accordion is a list with nested sections that expand open.
A special accordion object can be passed to the list parameter
that includes the menu items, styles for the sub menus
and properties to animate, shade and indent the sub menus.

SPACING, PADDING, INDENTING
These adjust depending on vertical or horizontal settings
The spacing is the distance between items (default 2)
The padding is the distance around the items but not between (default spacing)
So changing the spacing can seem to change the padding - but that can be overridden
There is also paddingVertical and paddingHorizontal that can be adjusted (default padding)
Indent only works with custom items in the list in left, right alignment or top, bottom valignment
This moves the items away from their alignment
There is also label indenting for items with labels - and labelIndentVertical and labelIndentHorizontal

NOTE: List can have a ZIM Organizer added with the organizer parameter
The organizer lets the user add, remove and move items up, down, to the top or the bottom
See: https://zimjs.com/docs?item=organizer
See: https://zimjs.com/explore/organizer.html
See: https://zimjs.com/ten/accordion.html
See: https://zimjs.com/ten/listcheckbox.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var list = new List({
	list:["Enormous", "Big", "Medium", "Small", "Puny"],
	viewNum:3, // this number will change the size of the list elements (default is 5)
}).center()
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 300) width of list
height - (default 200) height of list
list - (default Options 1-30) an array of strings, numbers or zim Label objects - these will be added to zim Tabs Buttons
	or include any DisplayObject with bounds - these will not get highlighted but will indicate a change event and selectedIndex
	currently objects with different sizes may not animateTo() properly - this will be fixed soon.
	A special Accordion object literal {} can be provided - see: https://zimjs.com/ten/accordion.html
		with the following properties:
		menu - a SIMPLE or COMPLEX hierarchy input - see ZIM Hierarchy() in the Code module
			note: if just providing the menu and the rest of the properties below are default
			then the Accordion object literal can be the SIMPLE or COMPLEX hierarchy input.
			In other words, no need to pass in {menu:{blah}} just pass in {blah}
		shade - (default .2) the alpha of indented shade in sub menus - or false for no shading
		shift - (default 15) the pixels to indent the shade - and left or right aligned text of sub menus - or false for no indenting
		dim - (default .1) the alpha of dark background overlay on sub menus - or false for no dimming
		bloom - (default 10) time in milliseconds for each submenu item to be added - or false to not animate sub menus in
		whither - (default 10) time in milliseconds for each submenu item to be removed - or false to not animate sub menus out
		subStyles - (default null) an array of style objects for each sublevel - with all the color and background color properties
	note: the Accordion List is currently incompatible with the Organizer, addTo() and removeFrom()
viewNum - (default 5) how many items to show in the width and height provided - for text list not custom objects...
vertical - (default true) set to false to make a horizontal list
currentSelected - (default false) set to true to show the current selection as highlighted
align - (default "center") horizontal align
valign - (default "center") vertical align
labelAlign - (default "center") horizontal align of the label only
labelValign - (default "center") vertical align of the label only
labelIndent - (default indent) indent of label when align or valign is set - usually same as indent unless custom objects are in list
labelIndentHorizontal - (default indent) horizontal indent of label when align or valign is set
labelIndentVertical - (default indent) vertical indent of label when align or valign is set
indent - (default 10) indent of items when align or valign is set and there are custom objects in list
spacing - (default 2) is the pixels between tab buttons
backgroundColor - (default "#777") the background color of the list elements (unless custom object)
rollBackgroundColor - (default "#555") the background color of the list element as rolled over
selectedBackgroundColor - (default "#444") the background color of the list element when selected
backdropColor - (default "#333") the background color of the list window (any CSS color)
color - (default "white") the text color of a deselected list element when not rolled over
selectedColor - (default color) the text color of the selected list element
rollColor - (default color) the rollover color (selected list elements do not roll over)
borderColor - (default #999) border color
borderWidth - (default 1) the thickness of the border
padding - (default 0) places the content in from edges of border (see paddingHorizontal and paddingVertical)
corner - (default 0) is the rounded corner of the list (does not accept corner array - scrollBars are too complicated)
swipe - (default auto/true) the direction for swiping set to none / false for no swiping
	also can set swipe to just vertical or horizontal
scrollBarActive - (default true) shows scrollBar (set to false to not)
scrollBarDrag - (default true) set to false to not be able to drag the scrollBar
scrollBarColor - (default borderColor) the color of the scrollBar
scrollBarAlpha - (default .3) the transparency of the scrollBar
scrollBarFade - (default true) fades scrollBar unless being used
scrollBarH - (default true) if scrolling in horizontal is needed then show scrollBar
scrollBarV - (default true) if scrolling in vertical is needed then show scrollBar
scrollBarOverlay - (default true) set to false to not overlay the scrollBar on the cotnent
	overlaying could hide content - but without overlay, content less than window size will show gap when no scrollBar
slide - (default true) Boolean to throw the content when drag/swipe released
slideDamp - (default .6) amount the slide damps when let go 1 for instant, .01 for long slide, etc.
slideSnap - (default "vertical") "auto" / true, "none" / false, "horizontal"
	slides past bounds and then snaps back to bounds when released
	vertical snaps when dragging up and down but not if dragging horizontal
shadowColor - (default rgba(0,0,0,.3)) the color of the shadow
shadowBlur - (default 20) set shadowBlur to -1 for no drop shadow
paddingHorizontal - (default padding) places content in from top bottom
paddingVertical - (default padding) places content in from left and right
scrollWheel - (default true) scroll vertically with scrollWheel
damp - (default null) set to .1 for instance to damp the scrolling
titleBar - (default null - no titleBar) a String or ZIM Label title for the list that will be presented on a titleBar across the top
titleBarColor - (default "black") the text color of the titleBar if a titleBar is requested
titleBarBackgroundColor - (default "rgba(0,0,0,.2)") the background color of the titleBar if a titleBar is requested
titleBarHeight - (default fit label) the height of the titleBar if a titleBar is requested
draggable - (default true if titleBar) set to false to not allow dragging titleBar to drag list
boundary - (default null) set to ZIM Boundary() object - or CreateJS.rectangle()
close - (default false) - a close X for the top right corner that closes the list when pressed
closeColor - (default #555) - the color of the close X if close is requested
excludeCustomTap - (default false) set to true to exclude custom buttons from tap() which would override existing tap() on the custom buttons
organizer - (default null) the ZIN Organizer for the list
checkBox - (default false) set to true to turn labeled list into a list of ZIM CheckBox objects - thanks Dale789 for the prompting!
	See: https://zimjs.com/ten/listcheckbox.html
	use selected.checkBox to get access to the selected CheckBox
	use the checkBoxes property to get a list of the CheckBox objects
	use setCheck(index, type), setChecks(type), getCheck(index) methods to manipulate
	use STYLE to set CheckBox size
clone - (default false) set to true to add clones of the list items rather than the items themselves
cancelCurrentDrag - (default false) - set to true to cancel window dragging when document window loses focus
	this functionality seems to work except if ZIM is being used with Animate - so we have left it turned off by default
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
animateTo(index, timePerItem) - animate list to index at given time per item (default 50ms) - returns object for chaining
addAt(items, index, clone) - an array of items to insert at an index in the list - returns object for chaining
	clone defaults to false - set to true to add a clone of the item or items to the list
removeAt(number, index) - remove a number of elements (default 1) from the list starting at and including the index - returns object for chaining
clear() - clears the list
first() - select first list element - returns object to chain
last() - select last list element - returns object to chain
setCheck(index, type) - set the CheckBox at an index to true or false (the type parameter)
setChecks(type) - set all CheckBoxes to true or false (the type parameter) returns object for chaining
getCheck(index) - get the checked value of the CheckBox at an index
cancelCurrentDrag() - stop current drag on list - but add dragging back again for next drag
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection
List.checkItem(text, width, paddingHorizontal, paddingVertical, color, rollColor, backgroundColor, rollBackgroundColor, selectedColor, selectedRollColor, selectedBackgroundColor, selectedRollBackgroundColor)
	A static method (use it like List.checkItem("hello", 30, 400, 10, 10, white, etc.))
	To add a checkItem to a plain list use:
	new List({list:["goodbye", List.checkItem("hello", 30, 400, 10, 10, white), "what?"]})

ALSO: All Window methods

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
selectedIndex - get or set the index of the selected list element
selectedIndexPlusPosition set the index and scroll the index into view - might be broken for lists with custom objects of different heights
selected - gets the current selected list object (ie. a Button)
	use selected.checkBox to access the selected CheckBox if checkBox parameter is true
text - gets or sets the current selected label text
label - gets current selected label object
items (or list) - read-only array of list button objects or objects in the list
	this will change from the list entered as parameters as strings are turned into tab buttons, etc.
	use addAt() and removeAt() methods to manipulate
	custom items can be accessed using item.content - as the item is a container with a backing then content
checkBoxes - read-only array of CheckBox objects if checkBox parameter is true
itemsText - read-only array of text for labels (or null element if no label)
itemWidth - the width of each item (unless custom items)
itemHeight - the height of each item (unless custom items)
length - read-only length of the list
tabs - a reference to the tabs object used in the list
vertical - read-only true if list is vertical or false if horizontal
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: All Window properties

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "change" event - then use selectedIndex or text to find data

ALSO: All Window events including "scrolling"

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+60.5

	zim.List = function(width, height, list, viewNum, vertical, currentSelected, align, valign, labelAlign, labelValign, labelIndent, labelIndentHorizontal, labelIndentVertical, indent, spacing, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, backdropColor, color, selectedColor, rollColor, selectedRollColor, borderColor, borderWidth, padding, corner, swipe, scrollBarActive, scrollBarDrag, scrollBarColor, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, scrollBarOverlay, slide, slideDamp, slideSnap, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, scrollWheel, damp, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, excludeCustomTap, organizer, checkBox, clone, cancelCurrentDrag, style, group, inherit) {
		var sig = "width, height, list, viewNum, vertical, currentSelected, align, valign, labelAlign, labelValign, labelIndent, labelIndentHorizontal, labelIndentVertical, indent, spacing, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, backdropColor, color, selectedColor, rollColor, selectedRollColor, borderColor, borderWidth, padding, corner, swipe, scrollBarActive, scrollBarDrag, scrollBarColor, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, scrollBarOverlay, slide, slideDamp, slideSnap, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, scrollWheel, damp, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, excludeCustomTap, organizer, checkBox, clone, cancelCurrentDrag, style, group, inherit";
		var duo; if (duo = zob(zim.List, arguments, sig, this)) return duo;
		z_d("60.5");

		this.group = group;
		var DS = style===false?{}:zim.getStyle("List", this.group, inherit);

		if (zot(width)) width=DS.width!=null?DS.width:300;
		if (zot(height)) height=DS.height!=null?DS.height:!zot(organizer)?200+organizer.height:200;
		if (zot(list)) list = list=DS.list!=null?DS.list:["Option 1", "Option 2", "Option 3", "Option 4", "Option 5", "Option 6", "Option 7", "Option 8", "Option 9", "Option 10"];
		if (list.length == 0) list=["%-&"];
		this.originalList = list;
		if (zot(viewNum)) viewNum=DS.viewNum!=null?DS.viewNum:5;
		if (zot(vertical)) vertical=DS.vertical!=null?DS.vertical:true;
		if (zot(currentSelected)) currentSelected=DS.currentSelected!=null?DS.currentSelected:true;
		var originalAlign = !zot(align) || !zot(labelAlign);
		if (zot(checkBox)) checkBox=DS.checkBox!=null?DS.checkBox:false;
		if (zot(align)) align=DS.align!=null?DS.align:(checkBox?"left":"center");
		if (zot(valign)) valign=DS.valign!=null?DS.valign:"center";
		if (zot(labelAlign)) labelAlign=DS.labelAlign!=null?DS.labelAlign:align;
		if (zot(labelValign)) labelValign=DS.labelValign!=null?DS.labelValign:valign;

		if (zot(indent)) indent=DS.indent!=null?DS.indent:(checkBox?0:10);
		if (zot(labelIndent)) labelIndent=DS.labelIndent!=null?DS.labelIndent:indent;
		if (zot(labelIndentHorizontal)) labelIndentHorizontal=DS.labelIndentHorizontal!=null?DS.labelIndentHorizontal:labelIndent;
		if (zot(labelIndentVertical)) labelIndentVertical=DS.labelIndentVertical!=null?DS.labelIndentVertical:labelIndent;

		if (zot(spacing)) spacing=DS.spacing!=null?DS.spacing:2;
		if (zot(backdropColor)) backdropColor = DS.backdropColor!=null?DS.backdropColor:"#333";
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"#777";
		if (zot(rollBackgroundColor)) rollBackgroundColor = DS.rollBackgroundColor!=null?DS.rollBackgroundColor:"#555";
		if (zot(selectedBackgroundColor)) selectedBackgroundColor = DS.selectedBackgroundColor!=null?DS.selectedBackgroundColor:"#444";
		if (zot(selectedRollBackgroundColor)) selectedRollBackgroundColor = DS.selectedRollBackgroundColor!=null?DS.selectedRollBackgroundColor:"#555";
		if (zot(color)) color = DS.color!=null?DS.color:"white";
		if (zot(rollColor)) rollColor = DS.rollColor!=null?DS.rollColor:color;
		if (zot(selectedColor)) selectedColor = DS.selectedColor!=null?DS.selectedColor:color;
		if (zot(selectedRollColor)) selectedRollColor = DS.selectedRollColor!=null?DS.selectedRollColor:rollColor;
		if (zot(backdropColor)) backdropColor = DS.backdropColor!=null?DS.backdropColor:backdropColor;

		var originalBorderColor = borderColor;
		var originalBorderWidth = borderWidth;
		if (zot(borderColor)) borderColor=DS.borderColor!=null?DS.borderColor:"#999";
		if (zot(borderWidth)) borderWidth=DS.borderWidth!=null?DS.borderWidth:1; // 0
		// if (vertical && zot(padding) && zot(paddingVertical)) paddingVertical = spacing;
		// if (!vertical && zot(padding) && zot(paddingHorizontal)) paddingHorizontal = spacing;
		if (zot(padding)) padding=DS.padding!=null?DS.padding:spacing;
		if (zot(corner)) corner=DS.corner!=null?DS.corner:0;
		if (zot(swipe)) swipe=DS.swipe!=null?DS.swipe:true; // true / auto, vertical, horizontal, false / none
		if (zot(scrollBarActive)) scrollBarActive=DS.scrollBarActive!=null?DS.scrollBarActive:true;
		if (zot(scrollBarDrag)) scrollBarDrag=DS.scrollBarDrag!=null?DS.scrollBarDrag:true;
		if (zot(scrollBarColor)) scrollBarColor=DS.scrollBarColor!=null?DS.scrollBarColor:borderColor;
		if (zot(scrollBarAlpha)) scrollBarAlpha=DS.scrollBarAlpha!=null?DS.scrollBarAlpha:.3;
		if (zot(scrollBarFade)) scrollBarFade=DS.scrollBarFade!=null?DS.scrollBarFade:true;
		if (zot(scrollBarH)) scrollBarH = DS.scrollBarH!=null?DS.scrollBarH:!vertical;
		if (zot(scrollBarV)) scrollBarV = DS.scrollBarV!=null?DS.scrollBarV:vertical;
		if (scrollBarDrag) scrollBarFade = DS.scrollBarFade!=null?DS.scrollBarFade:false;
		if (zot(scrollBarOverlay)) scrollBarOverlay = DS.scrollBarOverlay!=null?DS.scrollBarOverlay:true;
		if (zot(slide)) slide=DS.slide!=null?DS.slide:true;
		if (zot(slideDamp)) slideDamp=DS.slideDamp!=null?DS.slideDamp:.6;
		if (zot(slideSnap)) slideSnap=DS.slideSnap!=null?DS.slideSnap:vertical?"vertical":"horizontal"; // true / auto, vertical, horizontal, false / none
		if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:20;
		if (zot(paddingVertical)) paddingVertical=DS.paddingVertical!=null?DS.paddingVertical:padding;
		if (zot(paddingHorizontal)) paddingHorizontal=DS.paddingHorizontal!=null?DS.paddingHorizontal:padding;
		if (zot(scrollWheel)) scrollWheel = DS.scrollWheel!=null?DS.scrollWheel:true;
		if (zot(titleBar)) titleBar = DS.titleBar!=null?DS.titleBar:null;
		if (zot(titleBarColor)) titleBarColor = DS.titleBarColor!=null?DS.titleBarColor:null;
		if (zot(titleBarBackgroundColor)) titleBarBackgroundColor = DS.titleBarBackgroundColor!=null?DS.titleBarBackgroundColor:null;
		if (zot(titleBarHeight)) titleBarHeight = DS.titleBarHeight!=null?DS.titleBarHeight:35;
		if (zot(draggable)) draggable = DS.draggable!=null?DS.draggable:null;
		if (zot(boundary)) boundary = DS.boundary!=null?DS.boundary:null;
		if (zot(close)) close = DS.close!=null?DS.close:null;
		if (zot(closeColor)) closeColor = DS.closeColor!=null?DS.closeColor:null;
		if (zot(excludeCustomTap)) excludeCustomTap = DS.excludeCustomTap!=null?DS.excludeCustomTap:false;
		if (zot(clone)) clone = DS.clone!=null?DS.clone:false;

		if (titleBar === false) titleBar = null;
		this.vertical = vertical;

		var that = this;
		var originalHeight = height;

		// handle possible checkboxes
		if (checkBox) {
			zim.loop(list, function (item, i) {
				list[i] = zim.List.checkItem(item, null, width, "left", 10, 10, spacing, color, rollColor, selectedColor, selectedRollColor, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor);
			});
		}

		// handle possible accordion data
		if (list.constructor == {}.constructor) {
			var shade = !zot(list.shade)?list.shade:!zot(DS.shade)?DS.shade:.2;
			var dim = !zot(list.dim)?list.dim:!zot(DS.dim)?DS.dim:.1;
			var shift = !zot(list.shift)?list.shift:!zot(DS.shift)?DS.shift:15;
			var bloom = !zot(list.bloom)?list.bloom:!zot(DS.bloom)?DS.bloom:false;
			var whither = !zot(list.whither)?list.whither:!zot(DS.whither)?DS.whither:false;
			if (bloom === true) bloom = 10;
			if (whither === true) whither = 10;
			if (shift === true) shift = 15;
			if (shade === true) shade = .2;
			if (dim === true) dim = .1;
			var subStyles = !zot(list.subStyles)?list.subStyles:!zot(DS.subStyles)?DS.subStyles:{};
			if (!zot(list.menu)) {
				list = list.menu;
			}
			// how to handle if final hierarchy data is submitted
			var tree = new Hierarchy(list);
			list = tree.getLinearList();
			if (!originalAlign) align = labelAlign = "left";
		}

		this.align = align;
		this.valign = valign;
		this.indent = indent;

		if (!zot(organizer)) {
			height = height - organizer.height;
			organizer.list = that;
			organizer.setButtons();
			titleBarHeight += organizer.height
		}
		this.zimWindow_constructor(width, height, backdropColor, borderColor, borderWidth, padding, corner, swipe, scrollBarActive, scrollBarDrag, scrollBarColor, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, slide, slideDamp, slideSnap, true, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, scrollWheel, damp, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, cancelCurrentDrag, style, group, DS);
		this.type = "List";

		if (!zot(titleBar)) {
			this.titleBarLabel.pos(null,titleBarHeight-30,null,true);
			if (zot(titleBarHeight)) titleBarHeight = 30;
			height = height - titleBarHeight;
		}

		if (!zot(organizer)) {
			organizer.addTo(that).loc(0,-organizer.height)
		}


		that.itemWidth = vertical?(width-paddingHorizontal*2-(scrollBarActive?(scrollBarOverlay?0:6):0)):(width-paddingHorizontal*2)/viewNum;
		that.itemHeight = vertical?(height-paddingVertical*2)/viewNum:(height-paddingVertical*2-(scrollBarActive?(scrollBarOverlay?0:6):0));

		var tabs;
		function makeTabs(tabList) {
			tabs = that.tabs = new zim.Tabs({
				width:vertical?that.itemWidth:(that.itemWidth*list.length),
				height:vertical?that.itemHeight*list.length:that.itemHeight,
				tabs:tabList,
				spacing:spacing,
				vertical:vertical,
				backgroundColor:backgroundColor,
				rollBackgroundColor:rollBackgroundColor,
				selectedBackgroundColor:selectedBackgroundColor,
				selectedRollBackgroundColor:selectedRollBackgroundColor,
				color:color,
				rollColor:rollColor,
				selectedColor:selectedColor,
				selectedRollColor:selectedRollColor,
				backdropColor:backdropColor,
				currentEnabled:true,
				currentSelected:currentSelected,
				align:align,
				valign:valign,
				labelAlign:labelAlign,
				labelValign:labelValign,
				labelIndent:labelIndent,
				labelIndentHorizontal:labelIndentHorizontal,
				labelIndentVertical:labelIndentVertical,
				indent:indent,
				useTap:true,
				excludeCustomTap:excludeCustomTap,
				style:style,
				group:group,
				inherit:DS
			})
			// .mov(vertical?0:(spacing+2),vertical?(spacing+2):0);
			.mov(vertical?0:paddingHorizontal,vertical?paddingVertical:0);
			// var b = tabs.getBounds();
			// tabs.setBounds(0,0,vertical?b.width:(b.width+spacing*2+4),vertical?(b.height+spacing*2+4):b.height);
			that.add(tabs);
			// tabs.loc(paddingHorizontal, paddingVertical)
		}
		makeTabs(zim.copy(list, clone));

		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

		if (tree) {

			var ids = tree.getLinearIDs();
			loop(ids, function (id, i) {
				that.tabs.buttons[i].listZID = id;
			});

			// add these to single accordion object literal along with styles and data?
			// and then pass in to list parameter?


			loop(that.tabs.buttons, function (item) {
				var data = tree.getData(item.listZID);
				if (!isEmpty(data.list)) item.accordion = new Label({text:"+", align:"center", color:convertColor(color, "rgba", .6)}).center(item).alp(.7).pos(15, null, that.align!="right");
			});

			function applyAccordion(item, id, data) {
				item.listZID = id;
				if (item.label) {
					if (shade) new Rectangle(shift*(data.level+1), item.height).alp(shade).pos(0,0,that.align=="right",false,item);
					if (dim) new Rectangle(item.width, item.height).alp((data.level+1)*dim).addTo(item).bot().ord(1);
					if (shift && that.align!="center") item.label.x += (that.align=="right"?-1:1)*shift*(data.level+1);
				}
				var newData = tree.getData(id);
				newData.obj = item;
				if (newData.list && !isEmpty(newData.list)) item.accordion = new Label({text:"+", align:"center", color:convertColor(color, "rgba", .6)}).center(item).alp(.7).pos(15, null, that.align!="right");
			}

			that.tap(function () {
				if (!that.selected.accordion) return;
				var data = tree.getData(that.selected.listZID);
				if (data.open) { // close
					if (that.selected.accordion) that.selected.accordion.text = "+";
					data.open = false;
					var nextSibling = tree.getNextSibling(that.selected.listZID);
					if (zot(nextSibling)) var finalIndex = that.items.length-1;
					else {
						var finalIndex = zim.loop(that.items, function (item, i) {
							if (item.listZID == nextSibling) return i-1;
						});
					}
					if (whither) {
						var count = 0;
						that.enabled = false;
						interval(whither, function (obj) {
							that.removeAt(1, that.selectedIndex+finalIndex-that.selectedIndex-count);
							count++;
							if (obj.total == obj.count+1) that.enabled = true;
						}, finalIndex-that.selectedIndex, true);
					} else {
						that.removeAt(finalIndex-that.selectedIndex, that.selectedIndex+1);
					}
				} else { // open
					data.open = true;
					if (that.selected.accordion) that.selected.accordion.text = "-";
					var outer = tree.getLinearList(data.list);
					var ids = tree.getLinearIDs(data.list);
					if (!data.opened) {
						data.opened = true;
						var newStyles = subStyles[data.level];
						if (zot(newStyles)) newStyles = {};
						if (Array.isArray(outer)) {
							if (bloom) {
								var count = 0;
								that.enabled = false;
								interval(bloom, function (obj) {
									that.addAt(outer[count], that.selectedIndex+1+count, {
										backgroundColor:newStyles.backgroundColor,
										color:newStyles.color,
										rollBackgroundColor:newStyles.rollBackgroundColor,
										rollColor:newStyles.rollColor,
										selectedBackgroundColor:newStyles.selectedBackgroundColor,
										selectedColor:newStyles.selectedColor,
										selectedRollBackgroundColor:newStyles.selectedRollBackgroundColor,
										selectedRollColor:newStyles.selectedRollColor
									});
									var id = ids[count];
									var item = that.items[that.selectedIndex+1+count];
									applyAccordion(item, id, data);
									if (obj.total == obj.count+1) that.enabled = true;
									count++;
								}, outer.length, true);
							} else {
								that.addAt(outer, that.selectedIndex+1, {
									backgroundColor:newStyles.backgroundColor,
									color:newStyles.color,
									rollBackgroundColor:newStyles.rollBackgroundColor,
									rollColor:newStyles.rollColor,
									selectedBackgroundColor:newStyles.selectedBackgroundColor,
									selectedColor:newStyles.selectedColor,
									selectedRollBackgroundColor:newStyles.selectedRollBackgroundColor,
									selectedRollColor:newStyles.selectedRollColor
								});
								loop(ids, function (id, i) {
									var item = that.items[that.selectedIndex+1+i];
									applyAccordion(item, id, data);
								});
							}
						}
					} else {
						if (Array.isArray(outer)) {
							if (bloom) {
								var count = 0;
								that.enabled = false;
								interval(bloom, function (obj) {
									that.addAt(outer[count], that.selectedIndex+1+count);
									// that.selectedIndexPlusPosition = that.selectedIndex;
									if (obj.total == obj.count+1) that.enabled = true;
									count++;
								}, outer.length, true)
							} else {
								that.addAt(outer, that.selectedIndex+1);
							}
						}
					}
				}
			});

		}

		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



		var _selectedIndex;
		tabs.tap(function (e) {
			if (e.target.selectedIndex == that.selectedIndex) return;
			that.selectedIndex = tabs.selectedIndex;
			that.dispatchEvent("change");
			e.preventDefault();
		});

		this.getItemIndex = function(item) {
			return that.items.indexOf(item);
		}

		this.animateTo = function(index, timePerItem) {
			if (zot(index)) index = 0;
			if (zot(timePerItem)) timePerItem = 50;
			that.selectedIndex = index;
			var newPos = getScrollPosition(index);
			if (vertical) {
				var itemsToTravel = Math.abs(that.scrollY-newPos)/that.itemHeight;
				that.animate({scrollY:newPos}, itemsToTravel*timePerItem);
				// that.scrollY = newPos;
			} else {
				var itemsToTravel = Math.abs(that.scrollX-newPos)/that.itemWidth;
				that.animate({scrollX:newPos}, itemsToTravel*timePerItem);
				// that.scrollX = newPos;
			}
			return that;
		}

		this.addAt = function(items, index, style, clone) {
			that.tabs.addAt(copy(items, clone), index, style)
			// var b = tabs.getBounds();
			// tabs.setBounds(0,0,vertical?b.width:(b.width+spacing*2+4),vertical?(b.height+spacing*2+4):b.height);
			that.content.x = 0;
			that.content.y = 0;
			that.update();
			return that;
		}

		this.removeAt = function(num, index) {
			that.tabs.removeAt(num, index)
			var b = tabs.getBounds();
			tabs.setBounds(0,0,b.width,b.height);
			that.content.x = 0;
			that.content.y = 0;
			that.update();
			return that;
		}
		if (list[0]=="%-&" && list.length==1) that.removeAt(1,0);

		this.clear = function() {
			that.tabs.removeAt(that.length, 0);
			that.content.x = 0;
			that.content.y = 0;
			that.update();
			return that;
		}

		this.setCheck = function(index, type) {
			if (zot(index)) index = 0;
			if (zot(type)) type = true;
			that.items[index].checkBox.checked = type;
			that.items[index].content.zimOut();
		}

		this.getCheck = function(index) {
			if (zot(index)) index = 0;
			return that.items[index].checkBox.checked;
		}

		this.setChecks = function(type) {
			zim.loop(that.items, function (item) {
				item.checkBox.checked = type;
				item.content.zimOut();
			});
			return that;
		}

		Object.defineProperty(that, 'items', {
			get: function() {
				return that.tabs.buttons;
			},
			set: function(value) {
				if (zon) zog("List() - items is read only - use addAt() and removeAt() to change");
			}
		});

		Object.defineProperty(that, 'list', {
			get: function() {
				return that.tabs.buttons;
			},
			set: function(value) {
				if (zon) zog("List() - list is read only - use addAt() and removeAt() to change");
			}
		});

		Object.defineProperty(that, 'length', {
			get: function() {
				return that.tabs.buttons.length;
			},
			set: function(value) {
				if (zon) zog("List() - length is read only");
			}
		});

		Object.defineProperty(that, 'selectedIndex', {
			get: function() {
				return _selectedIndex;
			},
			set: function(value) {
				tabs.selectedIndex = value;
				// that.text = tabs.text;
				that.label = tabs.label;
				that.selected = tabs.selected;
				_selectedIndex = value;
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});
		if (currentSelected) that.selectedIndex = 0;

		Object.defineProperty(that, 'selectedIndexPlusPosition', {
			get: function() {
				return _selectedIndex;
			},
			set: function(value) {
				that.selectedIndex = value;
				_selectedIndex = value;
				if (vertical) that.scrollY = getScrollPosition(value);
				else that.scrollX = getScrollPosition(value);
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});
		function getScrollPosition(index) {
			// var w = (that.itemWidth+spacing)*(vertical?1:that.length);
			// var h = (that.itemHeight+spacing)*(vertical?that.length:1);
			// that.tabs.setBounds(w, h);

			if (vertical) {
				var newY = -(that.itemHeight+spacing)*index + height/2 - that.itemHeight/2;
				if ((that.itemHeight+spacing)*that.length < height) newY = 0;
				if (newY > 0) newY = 0;
				if ((that.itemHeight+spacing)*that.length > height && newY < -that.tabs.height+height-paddingVertical*2) newY = -that.tabs.height+height-paddingVertical*2;
				return newY;
			} else {
				var newX = -(that.itemWidth+spacing)*index + width/2 - that.itemWidth/2;
				if ((that.itemWidth+spacing)*that.length < width) newX = 0;
				if (newX > 0) newX = 0;
				if ((that.itemWidth+spacing)*that.length > width && newX < -that.tabs.width+width-paddingHorizontal*2) newX = -that.tabs.width+width-paddingHorizontal*2;
				return newX;
			}
		}

		Object.defineProperty(that, 'text', {
			get: function() {
				if (that.label) return that.label.text;
			},
			set: function(value) {
				if (that.label) that.label.text = value;
			}
		});

		Object.defineProperty(that, 'itemDown', {
			get: function() {
				return that.tabs.buttonDown
			},
			set: function(value) {

			}
		});

		Object.defineProperty(that, 'itemsText', {
			get: function() {
				var a = [];
				for (var i=0; i<that.tabs.buttons.length; i++) {
					var aa = that.tabs.buttons[i].label;
					if (aa && !zot(aa.text)) {
						a.push(aa.text);
					} else {
						a.push(null);
					}
				}
				return a;
			},
			set: function(value) {
				if (zon) zog("List() - itemsText is read only");
			}
		});
		if (currentSelected) that.selectedIndex = 0;

		Object.defineProperty(that, 'checkBoxes', {
			get: function() {
				var a = [];
				for (var i=0; i<that.tabs.buttons.length; i++) {
					var aa = that.tabs.buttons[i].checkBox;
					a.push(aa);
				}
				return a;
			},
			set: function(value) {
				if (zon) zog("List() - checkBoxes is read only");
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
			}
		});

		this.last = function() {
			this.selectedIndexPlusPosition = this.length-1;
			return this;
		}
		this.first = function() {
			this.selectedIndexPlusPosition = 0;
			return this;
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.List(width, originalHeight, zim.copy(that.originalList, true), viewNum, vertical, currentSelected, align, valign, labelAlign, labelValign, labelIndent, labelIndentHorizontal, labelIndentVertical, indent, spacing, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, backdropColor, color, selectedColor, rollColor, selectedRollColor, originalBorderColor, originalBorderWidth, padding, zim.copy(corner), swipe, scrollBarActive, scrollBarDrag, scrollBarColor, scrollBarAlpha, scrollBarFade, scrollBarH, scrollBarV, scrollBarOverlay, slide, slideDamp, slideSnap, shadowColor, shadowBlur, paddingHorizontal, paddingVertical, scrollWheel, damp, titleBar, titleBarColor, titleBarBackgroundColor, titleBarHeight, draggable, boundary, close, closeColor, excludeCustomTap, organizer, checkBox, clone, cancelCurrentDrag, style, this.group, inherit));
		}
	}
	zim.extend(zim.List, zim.Window, "clone", "zimWindow", false);

	zim.List.checkItem = function(label, size, width, align, paddingHorizontal, paddingVertical, spacing, color, rollColor, selectedColor, selectedRollColor, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, group, inherit) {
		var sig = "label, size, width, align, paddingHorizontal, paddingVertical, spacing, color, rollColor, selectedColor, selectedRollColor, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, group, inherit";
		var duo; if (duo = zob(zim.List.checkItem, arguments, sig)) return duo;

		var DS = zim.getStyle("CheckBox", group, inherit);
		if (zot(size)) size = DS.size!=null?DS.size:20;

		var DS = zim.getStyle("List", group, inherit);
		if (zot(width)) width = 300;
		if (zot(paddingHorizontal)) paddingHorizontal = 10;
		if (zot(paddingVertical)) paddingVertical = 10;

		if (zot(align)) align = DS.align!=null?DS.align:"center";
		if (zot(spacing)) spacing = DS.spacing!=null?DS.spacing:2;
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"#777";
		if (zot(rollBackgroundColor)) rollBackgroundColor = DS.rollBackgroundColor!=null?DS.rollBackgroundColor:"#555";
		if (zot(selectedBackgroundColor)) selectedBackgroundColor = DS.selectedBackgroundColor!=null?DS.selectedBackgroundColor:"#444";
		if (zot(selectedRollBackgroundColor)) selectedRollBackgroundColor = DS.selectedRollBackgroundColor!=null?DS.selectedRollBackgroundColor:"#555";
		if (zot(color)) color = DS.color!=null?DS.color:"white";
		if (zot(rollColor)) rollColor = DS.rollColor!=null?DS.rollColor:color;
		if (zot(selectedColor)) selectedColor = DS.selectedColor!=null?DS.selectedColor:color;
		if (zot(selectedRollColor)) selectedRollColor = DS.selectedRollColor!=null?DS.selectedRollColor:rollColor;

		var c = new Container();
		c.type = "CheckItem"
		c.checkBox = new CheckBox({size:size, label:label, color:color, tap:true}),
		c.backing = new Rectangle(width-spacing*2, c.checkBox.height+paddingVertical*2, backgroundColor).addTo(c);
		c.checkBox.center(c)
		if (align != "center" && align != "middle") c.checkBox.pos(paddingHorizontal,null,align=="right");
		c.backing.tap(function () {
			c.checkBox.toggle();
			c.zimOut(); // could do over() but like out()
		});
		c.checkBox.change(function () {
			c.zimOut();
		});
		c.zimOver = function() {
			c.backing.color = c.checkBox.checked?selectedRollBackgroundColor:rollBackgroundColor;
			c.checkBox.label.color = c.checkBox.checked?selectedRollColor:rollColor;
			if (c.stage) c.stage.update();
		};
		c.zimOut = function(){
			c.backing.color = c.checkBox.checked?selectedBackgroundColor:backgroundColor;
			c.checkBox.label.color = c.checkBox.checked?selectedColor:color;
			if (c.stage) c.stage.update();
		}
		c.backing.on("mouseover", c.zimOver);
		c.checkBox.on("mouseover", c.zimOver);
		c.backing.on("mouseout", c.zimOut);
		c.checkBox.on("mouseout", c.zimOut);

		return c;
	}
	//-60.5


/*--
zim.Stepper = function(list, width, backgroundColor, borderColor, borderWidth, label, color, vertical, arrows, corner, shadowColor, shadowBlur, continuous, display, press, hold, holdDelay, holdSpeed, draggable, dragSensitivity, dragRange, stepperType, min, max, step, step2, arrows2, arrows2Scale, keyEnabled, keyArrows, rightForward, downForward, style, group, inherit)

Stepper
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Lets you step through a list of numbers or strings with arrows and keyboard arrows.
Uses mousedown to activate and defaults to stepping while pressing down
and going faster if you drag away from your press.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var stepper = new Stepper();
stepper.on("change", function() {
	zog(stepper.selectedIndex);
	zog(stepper.currentValue);
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
list - (default 1-10) pass in an array of strings or numbers to display one at a time
width - (default 100) is the width of the text box (you can scale the whole stepper if needed)
color - (default "white") for the arrows and the text box
borderColor - (default null) stroke color for the box
borderWidth - (default 1 if borderColor) stroke thickness for the box
label - (default null) which can be used to define custom text properties
vertical - (default false) set to true if you want the arrows above and below the text
arrows - (default true) - use graphical arrows (also see keyArrows to turn off keyboard arrows)
corner - (default 10) is the radius of the text box corners - set to 0 for square corners
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
shadowColor - (default rgba(0,0,0,.3)) set to -1 for no drop shadow
shadowBlur - (default 14) value for shadow blur if shadow is set
continuous - (default false) set to true to loop around or go back past 0 index
display - (default true) set to false just to just show the arrows and not the value
press - (default true) will advance on label mousedown - set to false to not advance on mousedown
hold - (default true) set to false to not step with extended press down
holdDelay - (default 400 ms) time (milliseconds) to wait for first step with hold
holdSpeed - (default 200 ms) time (milliseconds) between steps as holding
draggable - (default true) set to false to not step when dragging
dragSensitivity - (default .1) .01 changes really quickly - 1 changes at base rate
dragRange - (default 200) absolute distance (pixels) from press the drag will reach maximum
stepperType - (default "list") list draws values from list parameters
	also stepperType "number", "letter" - these get ranges below
min - (default 0 for number and "A" for letter) the minimum value (can make min bigger than max) (not for list stepperType)
max - (default 100 for number and "Z" for letter) the maximum value (can make max smaller than min) (not for list stepperType)
step - (default 1) the step value each time - can be decimal (only positive, only for number stepperType)
step2 - (default set to step) the step value when dragging perpendicular to main horizontal or vertical direction
	step2 will run with draggable set to true or with arrows2 set below (only positive, only for number stepperType)
arrows2 - (default true if step2 different than step and stepperType number - else false) secondary arrows perpendicular to main horizontal or vertical direction
	arrows2 will activate step2 above (only for number stepperType)
arrows2Scale - (default .5) the scale relative to the main arrows
keyEnabled - (default true) set to false to disable keyboard search / number picker
keyArrows - (default true) set to false to disable keyboard arrows
rightForward - (default true) set to false to make left the forward direction in your list
downForward - (default true except if stepperType is "number" then default false) set to false to make up the forward direction in your list
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
next() - goes to next
prev() - goes to previous
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
selectedIndex - gets or sets the current index of the array and display
currentValue - gets or sets the current value of the array and display
currentValueEvent - gets or sets the current value and dispatches a "change" event if set and changed
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
stepperArray - gets or sets the list
containerPrev, containerNext - access to the zim Container that holds the arrows
arrowPrev, arrowNext - access to the zim Triangle objects
arrowPrev2, arrowNext2 - access to the zim Triangle objects for arrows2
min, max - only for number mode at the monent - currently, do not change the max to be less than the min
label - access to the Label
textBox - access to the text box backing shape
continuous - does the stepper loop
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
keyFocus - get or set the keyboard focus on the component - see also zim.KEYFOCUS
	will be set to true if this component is the first made or component is the last to be used
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties
and stage.update() in change event to see component change its graphics

EVENTS
dispatches a "change" event when changed by pressing an arrow or a keyboard arrow
(but not when setting selectedIndex or currentValue properties)

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+61
	zim.Stepper = function(list, width, backgroundColor, borderColor, borderWidth, label, color, vertical, arrows, corner,
			shadowColor, shadowBlur, continuous, display, press, hold, holdDelay, holdSpeed, draggable,
			dragSensitivity, dragRange, stepperType, min, max, step, step2, arrows2, arrows2Scale, keyEnabled, keyArrows, rightForward, downForward, style, group, inherit) {
		var sig = "list, width, backgroundColor, borderColor, borderWidth, label, color, vertical, arrows, corner, shadowColor, shadowBlur, continuous, display, press, hold, holdDelay, holdSpeed, draggable, dragSensitivity, dragRange, stepperType, min, max, step, step2, arrows2, arrows2Scale, keyEnabled, keyArrows, rightForward, downForward, style, group, inherit";
		var duo; if (duo = zob(zim.Stepper, arguments, sig, this)) return duo;
		z_d("61");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Stepper";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);
		if (zot(list)) list = DS.list!=null?DS.list:[0,1,2,3,4,5,6,7,8,9];
		if (zot(width)) width=DS.width!=null?DS.width:200;
		if (zot(backgroundColor)) backgroundColor=DS.backgroundColor!=null?DS.backgroundColor:"white";
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:null;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(color)) color = DS.color!=null?DS.color:"#555";
		if (zot(label)) label = DS.label!=null?DS.label:"";
		if (typeof label === "string" || typeof label === "number") label = new zim.Label({
			text:label, size:DS.size!=null?DS.size*2:64, color:color, align:DS.align!=null?DS.align:"center",
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", padding:"ignore", backgroundColor:"ignore",
			group:this.group
		});
		if (zot(vertical)) vertical=DS.vertical!=null?DS.vertical:false;
		if (zot(arrows)) arrows=DS.arrows!=null?DS.arrows:true;
		if (zot(corner)) corner=DS.corner!=null?DS.corner:16;
		if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:14;
		if (zot(continuous)) continuous=DS.continuous!=null?DS.continuous:false;
		if (zot(display)) display=DS.display!=null?DS.display:true;
		if (zot(press)) press=DS.press!=null?DS.press:true;
		if (zot(hold)) hold=DS.hold!=null?DS.hold:true;
		if (zot(holdDelay)) holdDelay=DS.holdDelay!=null?DS.holdDelay:400;
		if (zot(holdSpeed)) holdSpeed=DS.holdSpeed!=null?DS.holdSpeed:200;
		if (zot(draggable)) draggable=DS.draggable!=null?DS.draggable:true;
		if (zot(dragSensitivity) || dragSensitivity <= 0) dragSensitivity=DS.dragSensitivity!=null?DS.dragSensitivity:.1;
		if (zot(dragRange)) dragRange=DS.dragRange!=null?DS.dragRange:200;
		if (zot(stepperType)) stepperType=DS.stepperType!=null?DS.stepperType:"list";
		if (zot(min)) min=DS.min!=null?DS.min:0;
		if (zot(max)) max=DS.max!=null?DS.max:100;
		if (zot(step)) step=DS.step!=null?DS.step:1;
		if (zot(step2)) step2=DS.step2!=null?DS.step2:step;
		if (zot(arrows2) && step2 != step && stepperType == "number") arrows2=DS.arrows2!=null?DS.arrows2:true;
		if (zot(arrows2Scale)) arrows2Scale=DS.arrows2Scale!=null?DS.arrows2Scale:.5;
		if (zot(keyEnabled)) keyEnabled = DS.keyEnabled!=null?DS.keyEnabled:true;
		if (zot(keyArrows)) keyArrows = DS.keyArrows!=null?DS.keyArrows:true;
		if (zot(rightForward)) rightForward = DS.rightForward!=null?DS.rightForward:true;
		if (zot(downForward)) downForward = DS.downForward!=null?DS.downForward:(stepperType=="number"?false:true);

		var that = this;
		var index;
		var height = 100;
		var boxSpacing = height/4;

		var actualStep = step; // toggle between step and step2
		var numVal;
		var numDir = 1;
		var letterVal;
		var decimals;
		if (stepperType == "number") {
			min = Number(min);
			max = Number(max);
			if (min == NaN) min = 0;
			if (max == NaN) max = 100;
			if (max < min) {
				numDir = -1;
				var temp = max; // one day ES6
				max = min;
				min = temp;
				numVal = max;
			} else {
				numVal = min;
			}
			this.min = min;
			this.max = max;
			if (0 > min && 0 < max) numVal = 0;
			step = Math.abs(step);
			decimals = Math.max(getDecimals(step), getDecimals(step2));
		} else if (stepperType == "letter") {
			list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
			if (typeof min != "string") min = "A";
			if (typeof max != "string") max = "Z";
			min = min.substr(0,1).toUpperCase();
			max = max.substr(0,1).toUpperCase();
			var startLetter = list.indexOf(min);
			if (startLetter < 0) {min = "A"; startLetter = 0;}
			var endLetter = list.indexOf(max);
			if (endLetter < 0) {max = "Z"; endLetter = list.length;}
			if (endLetter < startLetter) {
				list.reverse();
				startLetter = list.length-1-startLetter;
				endLetter = list.length-1-endLetter;
			}
			list = list.splice(startLetter, endLetter-startLetter+1);
		} else {
			stepperType = "list";
		}

		function getDecimals(num) {
			var decimals = String(num).split(".")[1]
			if (decimals) {decimals = decimals.length} else {decimals = 0;};
			return decimals;
		}

		var rawEvent;
		var rawX = 0;
		var rawY = 0;

		if (draggable) {
			this.on("mousedown", function(e) {
				if (that.zimAccessibility && that.zimAccessibility.aria) return;
				this.stage.mouseMoveOutside = true;
				rawX = e.rawX/e.target.stage.scaleX;
				rawY = e.rawY/e.target.stage.scaleY;
				rawEvent = this.stage.on("stagemousemove", function(e){
					rawX = e.rawX/e.target.stage.scaleX;
					rawY = e.rawY/e.target.stage.scaleY;
				})
			}, null, true);
		}
		this.label = label;
		label.mouseChildren = false;
		label.mouseEnabled = false;

		var holdCheck = false;
		var delayTimeout;
		var speedTimeout;
		var roundTimeout;
		var clickCheck = false;
		var prev, arrowPrev, next, arrowNext, prev2, arrowPrev2, next2, arrowNext2;
		if (arrows || arrows2) {
			var arrowBacking = new createjs.Shape();
			arrowBacking.graphics.ss(borderColor).s(borderWidth).f("rgba(255,255,255,.11)").r(0,0,height*1.5,height*1.5);
			arrowBacking.regX = height*1.5 / 2;
			arrowBacking.regY = height*1.5 / 2 + boxSpacing/2;
		}
		if (arrows) {
			prev = this.containerPrev = new zim.Container({style:false});
			this.addChild(prev);
			prev.hitArea = arrowBacking;

			arrowPrev = this.arrowPrev = new zim.Triangle(height, height*.8, height*.8, backgroundColor, null, null, null, null, null, null, false);
			if (shadowColor != -1 && shadowBlur > 0) prev.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
			prev.addChild(arrowPrev);
			prev.cursor = "pointer";

			prev.on("mousedown", function(e) {
				if (that.zimAccessibility && that.zimAccessibility.aria) return;
				actualStep = step;
				var val = vertical?(downForward?1:-1):(rightForward?-1:1);
				doStep(val);
				go(val, null, null, e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY);
			})
			if (hold) prev.on("pressup", goEnd);

			if (vertical) {
				prev.rotation = 180;
				prev.x = width/2;
				if (display) {
					prev.y = prev.height + boxSpacing + height + prev.height/2 + boxSpacing;
				} else {
					prev.y = prev.height * 2;
				}
			} else {
				prev.rotation = -90;
				prev.x = prev.height/2;
				prev.y = prev.width/2;
			}
		}

		if (display) {
			var box = this.textBox = new zim.Shape({style:false});
			box.cursor = "pointer";
			this.addChild(box);
			box.setBounds(0, 0, width, height);
			if (borderColor != null) box.graphics.s(borderColor).ss(borderWidth);

			var cc = corner;
			if (!Array.isArray(cc)) cc = [corner, corner, corner, corner];
			box.graphics.f(backgroundColor).rc(0, 0, width, height, cc[0], cc[1], cc[2], cc[3]);
			// box.graphics.f(backgroundColor).rr(0, 0, width, height, corner);
			if (shadowColor != -1 && shadowBlur > 0) box.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);

			if (arrows) {
				if (vertical) {
					if (arrows) box.y = arrowPrev.height + boxSpacing;
				} else {
					if (arrows) box.x = arrowPrev.height + boxSpacing;
				}
			}

			this.addChild(label);
			if (list.length > 0) {
				// index = Math.floor(list.length/2)
				index = 0;
				label.text = list[index];
			}
			label.x = 50+box.x+box.getBounds().width/2;
			label.y = box.y+(box.getBounds().height-label.getBounds().height)/2;

			var lastValue;
			box.on("mousedown", function(e) {
				if (that.zimAccessibility && that.zimAccessibility.aria) return;
				lastValue = that.currentValue;
				if (press) doStep(1);
				go(1, true, null, e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY); // do decimals from box
				if (stepperType == "number") {
					clearTimeout(roundTimeout);
					clickCheck = true;
					roundTimeout = setTimeout(function() {
						clickCheck = false;
					}, 200);
				}
			});
			box.on("pressup", function() {
				if (that.zimAccessibility && that.zimAccessibility.aria) return;
				if (clickCheck) {
					numVal = Math.round(numVal);
					setLabel(numVal, numVal);
					if (that.currentValue != lastValue) that.dispatchEvent("change");
					lastValue = that.currentValue;
				}
			});
		} else {
			if (list.length > 0) {
				index = 0;
			}
		}

		if (arrows) {
			next = this.containerNext = new zim.Container({style:false});
			this.addChild(next);
			next.hitArea = arrowBacking.clone();

			arrowNext = this.arrowNext = new zim.Triangle(height, height*.8, height*.8, backgroundColor, null, null, null, null, null, null, false);
			if (shadowColor != -1 && shadowBlur > 0) next.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
			next.addChild(arrowNext);
			next.cursor = "pointer";

			next.on("mousedown", function(e) {
				if (that.zimAccessibility && that.zimAccessibility.aria) return;
				actualStep = step;
				var val = vertical?(downForward?-1:1):(rightForward?1:-1);
				doStep(val);
				go(val, null, null, e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY);
			});

			if (hold) next.on("pressup", goEnd);

			if (vertical) {
				next.rotation = 0;
				next.x = width/2;
				next.y = next.getBounds().height/2;
			} else {
				next.rotation = 90;
				if (display) {
					next.x = box.x + box.getBounds().width + next.getBounds().height/2 + boxSpacing;
				} else {
					next.x = prev.x + prev.getBounds().width;
				}
				next.y = next.getBounds().width/2;
			}
		}


		var holdX;
		var proportion;
		// pressdown and move mouse changes speed and direction of stepper
		function go(dir, both, dec, stageX, stageY) {
			if (hold) {
				holdX = stageX;
				holdY = stageY;
				if (holdX == 0) holdX = 1;
				if (holdY == 0) holdY = 1;
				if (!draggable) dragSensitivity = 1;
				proportion = new zim.Proportion(0, dragRange, holdSpeed, holdSpeed*dragSensitivity);
				var dragInput = holdSpeed;
				delayTimeout = setTimeout(function() {
					holdCheck=true;
					function doHold() {
						speedTimeout = setTimeout(function() {
							var dragDir = dir;
							if (draggable) {
								// only change direction if outside of 10 pixels from where pressed
								var diffX = Math.abs(rawX - holdX);
								var diffY = Math.abs(rawY - holdY);
								if (vertical) {
									if (!both && !dec) diffX = 0; // don't do decimals
									if (dec) diffY = 0;
								} else {
									if (!both && !dec) diffY = 0; // don't do decimals
									if (dec) diffX = 0;
								}
								if (diffX >= 10 || diffY >= 10) {
									if (diffX > diffY) {
										actualStep = vertical?step2:step;
										dragDir = rawX - holdX > 0 ? 1 : -1;
										if (!rightForward) dragDir*-1;
										dragInput = proportion.convert(Math.abs(holdX-rawX));
									} else {
										actualStep = vertical?step:step2;
										dragDir = rawY - holdY > 0 ? 1 : -1;
										if (stepperType == "number" || !downForward) {
											dragDir *= -1;
										}
										dragInput = proportion.convert(Math.abs(holdY-rawY));
									}
								}
							}
							doStep(dragDir);
							doHold();
						}, dragInput);
					}
					doHold();
				}, holdDelay);
			}
		}

		if (hold && display) box.on("pressup", goEnd);

		function goEnd() {
			if (that.zimAccessibility && that.zimAccessibility.aria) return;
			holdCheck = false;
			clearTimeout(delayTimeout);
			clearTimeout(speedTimeout);
		}

		if (arrows2) { // step2 arrows

			prev2 = this.prev2 = new zim.Container({style:false});
			prev2.hitArea = arrowBacking.clone();
			arrowPrev2 = this.arrowPrev2 = new zim.Triangle(height, height*.8, height*.8, "rgba(0,0,0,.2)", backgroundColor, 2, null, null, null, null, false);
			prev2.addChild(arrowPrev2);
			prev2.cursor = "pointer";
			prev2.sca(arrows2Scale);
			prev2.alpha = .5;
			prev2.on("mousedown", function(e) {
				if (that.zimAccessibility && that.zimAccessibility.aria) return;
				actualStep = step2;
				var val = vertical?(rightForward?-1:1):(downForward?1:-1);
				doStep(val);
				go(val, null, true, e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY);
			});
			if (hold) prev2.on("pressup", goEnd);

			next2 = this.next2 = new zim.Container({style:false});
			next2.hitArea = arrowBacking.clone();
			arrowNext2 = this.arrowNext2 = new zim.Triangle(height, height*.8, height*.8, "rgba(0,0,0,.2)", backgroundColor, 2, null, null, null, null, false);
			next2.addChild(arrowNext2);
			next2.cursor = "pointer";
			next2.sca(arrows2Scale);
			next2.alpha = .5;
			next2.on("mousedown", function(e) {
				if (that.zimAccessibility && that.zimAccessibility.aria) return;
				actualStep = step2;
				var val = vertical?(rightForward?1:-1):(downForward?-1:1);
				doStep(val);
				go(val, null, true, e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY);
			});
			if (hold) next2.on("pressup", goEnd);

			if (vertical) {
				prev2.y = this.height / 2;
				prev2.x = -prev2.width / 2 - boxSpacing*Math.max(.2, Math.min(1, arrows2Scale));
				prev2.rotation = 270;
				next2.y = this.height / 2;
				next2.x = this.width + next2.width/2 + boxSpacing*Math.max(.2, Math.min(1, arrows2Scale));
				next2.rotation = 90;
			} else {
				next2.x = this.width / 2;
				next2.y = -next2.height / 2 - boxSpacing*Math.max(.2, Math.min(1, arrows2Scale));
				next2.rotation = 0;
				prev2.x = this.width / 2;
				prev2.y = this.height + prev2.height/2 + boxSpacing*Math.max(.2, Math.min(1, arrows2Scale));
				prev2.rotation = 180;
			}
			this.addChild(prev2, next2);
		}

		// needed in setLabel()
		Object.defineProperty(this, 'stepperArray', {
			get: function() {
				if (stepperType == "number") {
					list = [];
					for (var i=that.min; i<=that.max; i+=Math.min(step, step2)) {
						list.push(zim.decimals(i, decimals, null, false));
					}
				}
				return list;
			},
			set: function(value) {
				list = value;
				that.selectedIndex = that.selectedIndex;
			}
		});

		Object.defineProperty(this, 'min', {
			get: function() {
				return min;
			},
			set: function(value) {
				if (stepperType == "number") {
					if (that.currentValue < value) that.currentValue = value;
					min = value;
					that.selectedIndex = that.selectedIndex;
				} else {
					min = value;
				}
			}
		});

		Object.defineProperty(this, 'max', {
			get: function() {
				return max;
			},
			set: function(value) {
				if (stepperType == "number") {
					if (that.currentValue > value) that.currentValue = value;
					max = value;
					that.selectedIndex = that.selectedIndex;
				} else {
					max = value;
				}
			}
		});

		setLabel(stepperType=="number"?numVal:list[index], stepperType=="number"?numVal:index);

		function doStep(n) {
			var text;
			var nextIndex;
			if (stepperType == "number") {
				var lastNumVal = numVal;
				numVal += actualStep * n * numDir;
				numVal = zim.decimals(numVal, decimals);
				if (!continuous) {
					if (numVal > that.max) {
						numVal = step==1?that.max:lastNumVal;
						if (display) box.cursor = "default";
					} else {
						if (display) box.cursor = "pointer";
					}
					if (numVal < that.min) {
						numVal = step==1?that.min:lastNumVal;
					}
				} else {
					if (numVal > that.max) {
						numVal = that.min;
					} else if (numVal < that.min) {
						numVal = that.max;
					}
				}
			} else {
				nextIndex = index + n;
				if (!continuous) {
					if (nextIndex > list.length-1) {
						if (display) box.cursor = "default";
						return;
					} else {
						if (display) box.cursor = "pointer";
					}
					if (nextIndex < 0) return;
				} else {
					if (nextIndex > list.length-1) nextIndex = 0;
					if (nextIndex < 0) nextIndex = list.length-1;
				}
				index = nextIndex;
			}
			setLabel(stepperType=="number"?numVal:list[index], stepperType=="number"?numVal:index);
			if (that.currentValue != lastValue) that.dispatchEvent("change");
			lastValue = that.currentValue;
		}


		Object.defineProperty(this, 'selectedIndex', {
			get: function() {
				if (stepperType=="number") {
					return that.stepperArray.indexOf(that.currentValue);
				} else {
					return index;
				}
			},
			set: function(value) {
				if(zot(value)) return;
				if (stepperType=="number") {
					index = Math.min(that.stepperArray.length-1, Math.max(0, value));
					numVal = that.stepperArray[index];
					setLabel(numVal, numVal);
				} else {
					value = Math.min(list.length-1, Math.max(0, value));
					index = value;
					setLabel(list[index], index);
				}
			}
		});

		Object.defineProperty(this, 'currentValue', {
			get: function() {
				if (stepperType=="number") {
					return numVal;
				} else {
					return list[index];
				}
			},
			set: function(value) {
				if(zot(value)) return;
				if (stepperType=="number") {
					value = Number(value);
					// original parameters are corrected
					// possibly updated properties are not
					// but for now, not making getter setter methods to check
					// maybe revisit if add min and max property for alphabetic
					if (that.max > that.min) {
						if (value > that.max || value < that.min) return;
					} else {
						if (value < that.max || value > that.min) return;
					}
					newIndex = that.stepperArray.indexOf(value);
					if (newIndex < 0) return;
					index = newIndex;
					numVal = that.stepperArray[index];
					setLabel(numVal, numVal);
				} else {
					if (list.indexOf(value) > -1) {
						value = list.indexOf(value);
					} else {return;}
					if (value == that.selectedIndex) return;
					index=value;
					setLabel(list[index], index);
				}
			}
		});

		Object.defineProperty(this, 'currentValueEvent', {
			get: function() {
				return that.currentValue;
			},
			set: function(value) {
				if (String(value) != String(that.currentValue)) {
					that.currentValue = value;
					that.dispatchEvent("change");
				}
			}
		});

		Object.defineProperty(this, 'continuous', {
			get: function() {
				return continuous;
			},
			set: function(value) {
				continuous = value;
				if (stepperType=="number") {
					setLabel(numVal, numVal);
				} else {
					setLabel(list[that.selectedIndex], that.selectedIndex);
				}
			}
		});


		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
				if (value) {
					if (stepperType=="number") {
						setLabel(numVal, numVal);
					} else {
						setLabel(list[that.selectedIndex], that.selectedIndex);
					}
					window.addEventListener("keydown", that.keyDownEvent);
				} else {
					greyPrev();
					greyNext();
					window.removeEventListener("keydown", that.keyDownEvent);
					if (display) label.mouseChildren = false;
					if (display) label.mouseEnabled = false;
				}
				if (next && ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && next.stage)) {
					next.stage.update();
				} else if (label && ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && label.stage)) {
					label.stage.update();
				}
			}
		});

		if (typeof KEYFOCUS !== typeof undefined) zim.KEYFOCUS = KEYFOCUS;
		Object.defineProperty(this, 'keyFocus', {
			get: function() {
				return zim.KEYFOCUS == that;
			},
			set: function(value) {
				zim.KEYFOCUS = that;
			}
		});
		if (!zim.KEYFOCUS) setFocus();
		this.on("mousedown", function() {setFocus()});
		function setFocus() {that.keyFocus = true; var d=document.activeElement; if (d) d.blur();}

		function setArrows() {
			prev.alpha = 1;
			arrowPrev.color = backgroundColor;
			prev.cursor = "pointer";
			next.alpha = 1;
			arrowNext.color = backgroundColor;
			next.cursor = "pointer";
			if (!continuous) {
				if (stepperType == "number") {
					if (index == that.min) {
						if (numDir > 0) {greyPrev();} else {greyNext()};
					}
					if (index == that.max) {
						if (numDir > 0) {greyNext();} else {greyPrev()};
					}
				} else {
					if (index == 0) vertical?greyNext():greyPrev();
					if (index == list.length-1) vertical?greyPrev():greyNext();
				}
			}
		}

		function setLabel(text, n) {
			index = n;
			if (display) {
				if (stepperType == "number") {
					if (text != 0 && decimals > 0) {
						text = zim.decimals(text, decimals, true);
					}
				}
				label.text = text;
				label.x = box.x+box.getBounds().width/2;
				label.y = box.y+(box.getBounds().height-label.getBounds().height)/2;
			}
			if (arrows) setArrows();
			if (next && ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && next.stage)) {
				next.stage.update();
			} else if (label && ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && label.stage)) {
				label.stage.update();
			}
			if (that.zimAccessibility) that.zimAccessibility.changeTitle(that, null, true);
		}

		function greyPrev() {
			if (!arrows) return;
			prev.alpha = .8;
			arrowPrev.color = "#aaa";
			prev.cursor = "default";
		}
		function greyNext() {
			if (!arrows) return;
			next.alpha = .8;
			arrowNext.color = "#aaa";
			next.cursor = "default";
		}

		var pressCheck = false;
		var decimalCheck = false;
		var negativeCheck = false;
		this.on("mousedown", function() {
			if (that.zimAccessibility && that.zimAccessibility.aria) return;
			that.focus = true;
			pressCheck = true;
			decimalCheck = false;
			negativeCheck = false;
		})

		this.keyDownEvent = function(e) {
			if (!that.stage) return;
			if ((that.zimAccessibility && that.focus) || (!that.zimAccessibility && that.keyFocus)) {
				if (!e) e = event;
				var k = e.keyCode;
				if (keyArrows) {
					if (k >= 37 && k <= 40) {
						var forwardVertical = downForward?40:38;
						var forwardHorizontal = rightForward?39:37;
						var backwardVertical = downForward?38:40;
						var backwardHorizontal = rightForward?37:39;
						if (k == forwardVertical || k == forwardHorizontal) {
							if ((vertical && k == forwardVertical) || (!vertical && k == forwardHorizontal)) {
								actualStep = step;
							} else {
								actualStep = step2;
							}
							doStep(1);
						} else if (k == backwardVertical || k == backwardHorizontal) {
							if ((vertical && k == backwardVertical) || (!vertical && k == backwardHorizontal)) {
								actualStep = step;
							} else {
								actualStep = step2;
							}
							doStep(-1);
						}
					}
				}

				if (keyEnabled) {
					if (stepperType=="number") { // 48-57, 96-105 190. 173-
						var num;
						if (!e.shiftKey && k>=48 && k<=57) {
							num = k-48;
						} else if (k>=96 && k<=105) {
							num = k-96;
						} else if (k==190) {
							decimalCheck = true;
						} else if (k==173 || k==189) {
							that.currentValue = that.currentValue * -1;
							if (that.currentValue != lastValue) that.dispatchEvent("change");
							lastValue = that.currentValue;
							negativeCheck = !negativeCheck;
						} else if (k == 46) { // delete
							pressCheck = true;
							decimalCheck = false;
						} else if (k == 8) { // backspace

						}
						if (pressCheck && !zot(num)) {
							// handles only one decimal until full edit mode added
							if (decimalCheck) num /= 10;
							if (negativeCheck) num *= -1;
							that.currentValue = num;
							pressCheck = false;
							if (that.currentValue != lastValue) that.dispatchEvent("change");
							lastValue = that.currentValue;
						} else if (!zot(num)) {
							if (decimalCheck) num = String(num / 10).substr(1);
							that.currentValue = Number(Math.floor(Number(label.text)) + String(num));
							if (that.currentValue != lastValue) that.dispatchEvent("change");
							lastValue = that.currentValue;
						}
					} else {
						that.currentValue = String.fromCharCode(e.keyCode);
						if (that.currentValue != lastValue) that.dispatchEvent("change");
						lastValue = that.currentValue;
					}
				}
			}
		}
		window.addEventListener("keydown", this.keyDownEvent);

		this.next = function() {
			doStep(1);
		}

		this.prev = function() {
			doStep(-1);
		}

		if (style!==false) zimStyleTransforms(this, DS);


		this.clone = function() {
			return that.cloneProps(new zim.Stepper(list, width, backgroundColor, borderColor, borderWidth, label.clone(), color, vertical, arrows, corner, shadowColor, shadowBlur, continuous, display, press, hold, holdDelay, holdSpeed, draggable, dragSensitivity, dragRange, stepperType, min, max, step, step2, arrows2, arrows2Scale, keyEnabled, keyArrows, rightForward, downForward, style, this.group, inherit));
		}

		this.dispose = function() {
			window.removeEventListener("keydown", that.keyDownEvent);
			if (that.stage) that.stage.off(rawEvent);
			this.zimContainer_dispose();
			return true;
		}
	}
	zim["z"+"ut"] = function(e) { // patch for ZIM Distill
		if (!zot(e) && e["ke"+"y"]) {
			zim.async("ht"+"tps://zim"+"js.com/"+"gam"+"da"+"ta."+"ph"+"p?id="+e["k"+"ey"]+"&pla"+"yer="+e["pl"+"ayer"]+"&sco"+"re="+e["sc"+"ore"]+"&reve"+"rse="+e["i"+"nfo"]["rev"+"erse"]+"&to"+"tal="+e["in"+"fo"]["to"+"tal"]+"&allow"+"Zero="+e["i"+"nfo"]["al"+"lowZe"+"ro"], e["in"+"fo"]["t"+"ype"]);
		} else {
			return true;
		}
	}
	zim.extend(zim.Stepper, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-61

/*--
zim.Slider = function(min, max, step, button, barLength, barWidth, barColor, vertical, useTicks, inside, keyArrows, keyArrowsStep, keyArrowsH, keyArrowsV, damp, style, group, inherit)

Slider
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A traditional slider - will give values back based on min and max and position of button (knob).

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var slider = new Slider({step:1});
slider.center(stage);
slider.on("change", function() {
	zog(slider.currentValue); // 0-10 in steps of 1
});
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
min - (default 0) the minimum value for the slider
max - (default 10) the maximum value for the slider
step - (default 0) 0 is continuous decimal - 1 would provide steps of 1, 2 would provide steps of 2, etc.
button - (default small button with no label) - a Button
barLength - (default 300) the length of the bar (the slider slides along its length)
barWidth - (default 3) the width of the bar (how fat the bar is)
barColor - (default "#666") the color of the bar (any CSS color)
vertical - (default false) set to true to make slider vertical
useTicks - (default false) set to true to show small ticks for each step (step > 0)
inside - (default false) set to true to fit button inside bar (need to manually adjust widths)
keyArrows - (default true) set to false to disable keyboard arrows
keyArrowsStep - (default 1% of max-min) number to increase or decrease value when arrow is used
	if step is set, then this value is ignored and set to step
keyArrowsH - (default true) use left and right arrows when keyArrows is true
keyArrowsV - (default true) use up and down arrows when keyArrows is true
damp - (default null) set to value such as .1 to damp the slider currentValue
	use with Ticker rather than "change" event - eg:
	Ticker.add(function () {circle.x = slider.currentValue;});
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
currentValue - gets or sets the current value of the slider
currentValueEvent - gets or sets the current value and dispatches a "change" event if set and changed
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
min, max, step - read only - the assigned values
bar - gives access to the bar Rectangle
button - gives access to the Button
ticks - gives access to the ticks (to position these for example)
keyArrowsH, keyArrowsV - get or set the type of arrow keys to use (helpful for when cloning)
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
keyFocus - get or set the keyboard focus on the component - see also zim.KEYFOCUS
	will be set to true if this component is the first made or component is the last to be used
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties
and stage.update() in change event to see component change its graphics

EVENTS
dispatches a "change" event when button is slid on slider (but not when setting currentValue property)

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+62
	zim.Slider = function(min, max, step, button, barLength, barWidth, barColor, vertical, useTicks, inside, keyArrows, keyArrowsStep, keyArrowsH, keyArrowsV, damp, style, group, inherit) {
		var sig = "min, max, step, button, barLength, barWidth, barColor, vertical, useTicks, inside, keyArrows, keyArrowsStep, keyArrowsH, keyArrowsV, damp, style, group, inherit";
		var duo; if (duo = zob(zim.Slider, arguments, sig, this)) return duo;
		z_d("62");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Slider";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(min)) min = DS.min!=null?DS.min:0;
		if (zot(max)) max = DS.max!=null?DS.max:10;
		if (max-min == 0) {zog("ZIM Slider range must not be 0"); return;}
		if (zot(step)) step = DS.step!=null?DS.step:0;
		if (zot(barLength)) barLength = DS.barLength!=null?DS.barLength:300;
		if (zot(barWidth)) barWidth = DS.barWidth!=null?DS.barWidth:3;
		if (zot(barColor)) barColor = DS.barColor!=null?DS.barColor:"#666";
		if (zot(vertical)) vertical = DS.vertical!=null?DS.vertical:false;
		if (zot(useTicks)) useTicks = DS.useTicks!=null?DS.useTicks:false;
		if (zot(inside)) inside = DS.inside!=null?DS.inside:false;
		if (zot(keyArrows)) keyArrows = DS.keyArrows!=null?DS.keyArrows:true;
		if (zot(keyArrowsH)) keyArrowsH = DS.keyArrowsH!=null?DS.keyArrowsH:true;
		if (zot(keyArrowsV)) keyArrowsV = DS.keyArrowsV!=null?DS.keyArrowsV:true;
		if (zot(keyArrowsStep)) keyArrowsStep = DS.keyArrowsStep!=null?DS.keyArrowsStep:(max-min)/100;
		if (zot(damp)) damp = DS.damp!=null?DS.damp:false;
		var stage;

		var borderCheck = !(!zot(DS.backing)&&DS.backing.type!="Pattern");
		if (zot(button)) {
			var w = 30; var h = 40;
			if (vertical) {w = 50; h = 40;}
			button = DS.button!=null?DS.button:new zim.Button({
				width:DS.width!=null?DS.width:w,
				height:DS.height!=null?DS.height:h,
				label:"",
				backgroundColor: DS.backgroundColor!=null?DS.backgroundColor:"#fff",
				rollBackgroundColor: DS.rollBackgroundColor!=null?DS.rollBackgroundColor:"#ddd",
				borderColor: DS.borderColor!=null?DS.borderColor:(borderCheck?"#666":null),
				borderWidth: DS.borderWidth!=null?DS.borderWidth:(borderCheck?1:null),
				corner: DS.corner!=null?DS.corner:0,
				backing: DS.backing!=null?DS.backing.clone():null,
				rollBacking: DS.rollBacking!=null?DS.rollBacking.clone():null,
				hitPadding:30,
				style:false
			});
		}
		button.rollPersist = true;

		var width; var height;
		if (vertical) {
			width = button.width;
			if (inside) {
				height = barLength;
				this.setBounds(0, 0, width, height);
			} else {
				height = barLength + button.height;
				this.setBounds(-button.width/2, -button.height/2, width, height);
			}
		} else {
			height = Math.max(button.height, barWidth);
			if (inside) {
				width = barLength;
				this.setBounds(0, 0, width, height);
			} else {
				width = barLength+button.width;
				this.setBounds(-button.width/2, -button.height/2, width, height);
			}
		}

		var that = this;
		var myValue = min;
		var lastValue = 0; // does not include min so always starts at 0
		this.button = button;
		this.cursor = "pointer";

		var bar, rect, bounds, ticks, g;

		if (useTicks && step != 0) {
			ticks = this.ticks = new zim.Shape({style:false});
			this.addChild(ticks);
			g = ticks.graphics;
			g.ss(1).s(barColor);
			var stepsTotal = Math.round((max - min) / step);
			var newStep = (max - min) / stepsTotal;
			if (newStep != step) {if (zon) zog("zim.Slider() : non-divisible step ("+step+") adjusted");}
			step = newStep;
			if (inside) {
				var spacing = (barLength - ((vertical) ? button.height : button.width)) / Math.abs(stepsTotal);
			} else {
				var spacing = barLength / Math.abs(stepsTotal);
			}
		}

		if (vertical) {
			var start = (inside) ? button.height / 2 : 0;
			bar = this.bar = new zim.Rectangle(barWidth, barLength, barColor, null, null, null, null, null, false);
			bar.expand(20,0);
			bar.centerReg(this);
			button.centerReg(this);
			bounds = bar.getBounds();
			rect = new createjs.Rectangle(bar.x, bounds.y+start, 0, bounds.height-start*2);
			if (useTicks && step != 0) {
				for (var i=0; i<=Math.abs(stepsTotal); i++) {
					g.mt(0, start+spacing*i).lt(20, start+spacing*i);
				}
				ticks.x = bar.x+15;
			}
		} else {
			var start = (inside) ? button.width / 2 : 0;
			bar = this.bar = new zim.Rectangle(barLength, barWidth, barColor, null, null, null, null, null, false);
			bar.expand(0,20);
			bar.centerReg(this);
			button.centerReg(this);
			bounds = bar.getBounds();
			rect = new createjs.Rectangle(bounds.x+start, bar.y, bounds.width-start*2, 0);
			if (useTicks && step != 0) {
				for (var i=0; i<=Math.abs(stepsTotal); i++) {
					g.mt(start+spacing*i,0).lt(start+spacing*i,-20);
				}
				ticks.y = bar.y-10;
			}
		}
		button.x = rect.x;
		button.y = rect.y;

		function snap(v) {
			if (step == 0) return v;
			return Math.round(v/step)*step;
		}

		var diffX, diffY;
		button.on("mousedown", function(e) {
			stage = e.target.stage;
			that.focus = true;
			var point = that.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
			diffX = point.x - button.x;
			diffY = point.y - button.y;
			if (that.stage) that.stage.mouseMoveOutside = true;
		});

		button.on("pressmove", function(e) {
			setValue(e);
		});
		function setValue(e) {
			var point = that.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
			var p = checkBounds(point.x-diffX, point.y-diffY, rect);
			if (vertical) {
				button.x = p.x;
				myValue = snap((p.y-rect.y) / rect.height * (min - max));
				button.y = rect.y + myValue * rect.height / (min - max);
				myValue += max;
				if (button.y != lastValue) {
					that.dispatchEvent("change");
				}
				lastValue = button.y;
			} else {
				myValue = snap((p.x-rect.x) / rect.width * (max - min));
				button.x = rect.x + myValue * rect.width / (max - min);
				myValue += min;
				button.y = p.y;
				if (button.x != lastValue) {
					that.dispatchEvent("change");
				}
				lastValue = button.x;
			}
			setAccessibility();
			if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
		};

		function sign(n) {return n > 0 ? 1 : -1;}

		function setAccessibility() {
			if (that.zimAccessibility) that.zimAccessibility.changeTitle(that, null, true);
		}

		function checkBounds(x,y,rect) {
			x = Math.max(rect.x, Math.min(rect.x+rect.width, x));
			y = Math.max(rect.y, Math.min(rect.y+rect.height, y));
			return {x:x,y:y}
		}

		bar.on("mousedown", function(e) {
			diffX = 0; // button.width/2;
			diffY = 0; // button.height/2;
			if (that.zimAccessibility && that.zimAccessibility.aria) return;
			setValue(e);
		});

		var myDampedValue;
		var dampObject;
		if (damp) {
			myDampedValue = min;
			dampObject = new zim.Damp(myDampedValue, damp);
			that.ticker = zim.Ticker.add(function () {
				myDampedValue = dampObject.convert(myValue);
			});
		}

		Object.defineProperty(this, 'currentValue', {
			get: function() {
				return damp?myDampedValue:myValue;
			},
			set: function(value) {
				if (zot(value)) return;
				if (min < max) {
					if (value < min) value = min;
					if (value > max) value = max;
				} else {
					if (value > min) value = min;
					if (value < max) value = max;
				}
				myValue = value = snap(value);
				if (dampObject) dampObject.immediate(myValue);
				if (vertical) {
					button.y = (value - max) / (min - max) * rect.height + start;
					lastValue = button.y;
				} else {
					button.x = (value - min) / (max - min) * rect.width + start;
					lastValue = button.x;
				}
				setAccessibility();
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		Object.defineProperty(this, 'currentValueEvent', {
			get: function() {
				return damp?myDampedValue:myValue;
			},
			set: function(value) {
				if (value != that.currentValue) {
					that.currentValue = value;
					that.dispatchEvent("change");
				}
			}
		});

		Object.defineProperty(this, 'min', {
			get: function() {
				return min;
			},
			set: function(value) {
				if (zon) zog("min is read only");
			}
		});

		Object.defineProperty(this, 'max', {
			get: function() {
				return max;
			},
			set: function(value) {
				if (zon) zog("max is read only");
			}
		});

		Object.defineProperty(this, 'step', {
			get: function() {
				return step;
			},
			set: function(value) {
				if (zon) zog("step is read only");
			}
		});

		Object.defineProperty(this, 'keyArrowsH', {
			get: function() {
				return keyArrowsH;
			},
			set: function(value) {
				keyArrowsH = value;
			}
		});

		Object.defineProperty(this, 'keyArrowsV', {
			get: function() {
				return keyArrowsV;
			},
			set: function(value) {
				keyArrowsV = value;
			}
		});

		if (typeof KEYFOCUS !== typeof undefined) zim.KEYFOCUS = KEYFOCUS;
		Object.defineProperty(this, 'keyFocus', {
			get: function() {
				return zim.KEYFOCUS == that;
			},
			set: function(value) {
				zim.KEYFOCUS = that;
			}
		});
		if (keyArrows && !zim.KEYFOCUS) setFocus();
		this.on("mousedown", function() {if (keyArrows) setFocus()});
		function setFocus() {that.keyFocus = true; var d=document.activeElement; if (d) d.blur();}

		var leftCheck = false; var downCheck = false; var rightCheck = false; var upCheck = false;
		this.keyDownEvent = function(e) {
			if (!that.stage) return;
			if ((that.zimAccessibility && that.focus) || (!that.zimAccessibility && that.keyFocus)) {
				if (e.keyCode == 37 && keyArrowsH) leftCheck = true;
				else if (e.keyCode == 40 && keyArrowsV) downCheck = true;
				else if (e.keyCode == 39 && keyArrowsH) rightCheck = true;
				else if (e.keyCode == 38 && keyArrowsV) upCheck = true;
				if (that.keyInterval == null && (leftCheck || downCheck || rightCheck || upCheck)) {
					checkKey();
					// add traditional keydown delay
					that.keyTimeout = setTimeout(function() {
						if (that.keyInterval == null && (leftCheck || downCheck || rightCheck || upCheck)) that.keyInterval = setInterval(checkKey, 40);
					}, 140);
				}
			}
		}
		function checkKey() {
			if (leftCheck || downCheck) {
				if (step > 0) that.currentValueEvent -= step * sign(max-min);
				else that.currentValueEvent -= keyArrowsStep * sign(max-min);
			}
			if (rightCheck || upCheck) {
				if (step > 0) that.currentValueEvent += step * sign(max-min);
				else that.currentValueEvent += keyArrowsStep * sign(max-min);
			}
		}
		window.addEventListener("keydown", this.keyDownEvent);
		that.keyUpEvent = function(e) {
			if (e.keyCode == 37) leftCheck = false;
			else if (e.keyCode == 40) downCheck = false;
			else if (e.keyCode == 39) rightCheck = false;
			else if (e.keyCode == 38) upCheck = false;
			if (that.keyInterval != null && !leftCheck && !downCheck && !rightCheck && !upCheck) {
				clearInterval(that.keyInterval);
				that.keyInterval = null;
			}
		}
		window.addEventListener("keyup", this.keyUpEvent);

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
				if (value) {
					window.addEventListener("keydown", that.keyDownEvent);
					window.addEventListener("keyup", that.keyUpEvent);
				} else {
					window.removeEventListener("keydown", that.keyDownEvent);
					window.removeEventListener("keyup", that.keyUpEvent);
				}
			}
		});
		if (style!==false) zimStyleTransforms(this, DS);

		this.clone = function() {
			return that.cloneProps(new zim.Slider(min, max, step, button.clone(), barLength, barWidth, barColor, vertical, useTicks, inside, keyArrows, keyArrowsStep, keyArrowsH, keyArrowsV, damp, style, this.group, inherit));
		}

		this.dispose = function() {
			window.removeEventListener("keydown", that.keyDownEvent);
			window.removeEventListener("keyup", that.keyUpEvent);
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.Slider, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-62

/*--
zim.Dial = function(min, max, step, width, backgroundColor, indicatorColor, indicatorScale, indicatorType, innerCircle, innerScale, useTicks, innerTicks, tickColor, limit, keyArrows, keyArrowsStep, keyArrowsH, keyArrowsV, continuous, continuousMin, continuousMax, damp, style, group, inherit);

Dial
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A traditional dial - will give values back based on min and max and position of dial.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var dial = new Dial({step:1, color:"violet"});
dial.center(stage);
dial.on("change", function() {
	zog(dial.currentValue); // 1-10 in steps of 1
});
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
min - (default 0) the minimum value for the dial
max - (default 10) the maximum value for the dial
step - (default 1) 1 provides steps of 1, 0 is continuous decimal, 2 would provide steps of 2, etc.
width - (default 100) the width of the dial (diameter)
backgroundColor - (default "#666") the background color of the dial
indicatorColor - (default "#222") the color of the indicator
indicatorScale - (default 1) the scale of the indicator
indicatorType - (default "arrow" or "triangle") can also be "dot" or "circle", and "line" or "rectangle"
innerCircle - (default true) gives an inner knob look - set to false for flat
innerScale - (default 1) can be adjusted along with indicatorScale to get a variety of looks
useTicks - (default true) will show lines for ticks if step is set
innerTicks (default false) set to true to put the ticks inside if step is set
tickColor - (default indicatorColor) set the tick color if ticks are set
limit - (default true) stops dial from spinning right around - set to false to not limit dial
keyArrows - (default true) set to false to disable keyboard arrows
keyArrowsStep - (default 1% of max-min) number to increase or decrease value when arrow is used
	if step is set, then this value is ignored and set to step
keyArrowsH - (default true) use left and right arrows when keyArrows is true
keyArrowsV - (default true) use up and down arrows when keyArrows is true
continuous - (default false) this turns the dial into a continuous dial from the min at the top
	The (max-min)/360 give a delta value per degree
	and as the dial goes clockwise it adds the delta and as it goes counterclockwise it subtracts the delta
	The steps are still used or not if set to zero
	The min and max are no longer a real min and max - see the continuousMin and continuousMax below
	limit is ignored or set to false when continuous is true
continuousMin - (default null) set to Number to limit the minimum total value of the dial when continuous is true
continuousMax - (default null) set to Number to limit the maximum total value of the dial when continuous is true
damp - (default null) set to value such as .1 to damp the slider currentValue
	IGNORED when limit set to false - otherwise may damp incorrectly
	use with Ticker rather than "change" event - eg:
	Ticker.add(function () {circle.x = slider.currentValue;});
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
currentValue - gets or sets the current value of the dial
currentValueEvent - gets or sets the current value and dispatches a "change" event if set and changed
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
min, max - read only assigned values unless continuous is true - then write enabled
step - read only - the assigned values
continuous - gets a boolean as to whether the continuous is true (read only)
continuousMin - get or set the continuousMin if continuous is set to true
continuousMax - get or set the continuousMax if continuous is set to true
backing - gives access to the dial backing Circle
inner and inner2 give access to any inner circles
ticks - gives access to the ticks (to scale these for example)
indicator - gives access to the indicator container with registration point at the dial center
indicatorShape - gives access to the shape on the end of the indicator (zim Triangle, Circle, Rectangle)
keyArrowsH, keyArrowsV - get or set the type of arrow keys to use (helpful for when cloning)
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
keyFocus - get or set the keyboard focus on the component - see also zim.KEYFOCUS
	will be set to true if this component is the first made or component is the last to be used
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties
and stage.update() in change event to see component change its graphics

EVENTS
dispatches a "change" event when dial changes value (but not when setting currentValue property)

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+63
	zim.Dial = function(min, max, step, width, backgroundColor, indicatorColor, indicatorScale, indicatorType, innerCircle, innerScale, useTicks, innerTicks, tickColor, limit, keyArrows, keyArrowsStep, keyArrowsH, keyArrowsV, continuous, continuousMin, continuousMax, damp, style, group, inherit) {
		var sig = "min, max, step, width, backgroundColor, indicatorColor, indicatorScale, indicatorType, innerCircle, innerScale, useTicks, innerTicks, tickColor, limit, keyArrows, keyArrowsStep, keyArrowsH, keyArrowsV, continuous, continuousMin, continuousMax, damp, style, group, inherit";
		var duo; if (duo = zob(zim.Dial, arguments, sig, this)) return duo;
		z_d("63");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Dial";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(min)) min = DS.min!=null?DS.min:0;
		if (zot(max)) max = DS.max!=null?DS.max:10;
		if (max-min == 0) {zog("ZIM Dial range must not be 0"); return;}
		if (zot(step)) step = DS.step!=null?DS.step:1;
		if (zot(width)) width = DS.width!=null?DS.width:100;
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"#666";
		if (zot(indicatorColor)) indicatorColor = DS.indicatorColor!=null?DS.indicatorColor:"#222";
		if (zot(indicatorScale)) indicatorScale = DS.indicatorScale!=null?DS.indicatorScale:1;
		if (zot(indicatorType)) indicatorType = DS.indicatorType!=null?DS.indicatorType:"arrow";
		if (zot(innerCircle)) innerCircle = DS.innerCircle!=null?DS.innerCircle:true;
		if (zot(innerScale)) innerScale = DS.innerScale!=null?DS.innerScale:.5;
		if (zot(useTicks)) useTicks = DS.useTicks!=null?DS.useTicks:true;
		if (zot(innerTicks)) innerTicks = DS.innerTicks!=null?DS.innerTicksinnerTicks:false;
		if (zot(tickColor)) tickColor = DS.tickColor!=null?DS.tickColor:indicatorColor;
		if (zot(limit)) limit = DS.limit!=null?DS.limit:true;
		if (zot(keyArrows)) keyArrows = DS.keyArrows!=null?DS.keyArrows:true;
		if (zot(keyArrowsStep)) keyArrowsStep = DS.keyArrowsStep!=null?DS.keyArrowsStep:(max-min)/100;
		if (zot(keyArrowsH)) keyArrowsH = DS.keyArrowsH!=null?DS.keyArrowsH:true;
		if (zot(keyArrowsV)) keyArrowsV = DS.keyArrowsV!=null?DS.keyArrowsV:true;
		if (zot(continuous)) continuous = DS.continuous!=null?DS.continuous:false;
		if (continuous) limit = DS.limit!=null?DS.limit:false; // continuous sets the limit to false
		if (zot(damp)) damp = DS.damp!=null?DS.damp:false;
		if (limit == false) damp = null;
		var stage;

		var that = this;
		this.cursor = "pointer";

		var r = width / 2;
		var myValue = min; // includes the min
		var lastValue = 0; // does not include min (so always starts at 0)

		var backing = this.backing = new zim.Circle(r, backgroundColor, null, null, null, null, null, false);
		this.addChild(backing);

		if (innerCircle) {
			var ic = (innerTicks) ? "rgba(0,0,0,.2)" : "rgba(0,0,0,.1)";
			if (backgroundColor=="black"||backgroundColor=="#000"||backgroundColor=="#000000"||backgroundColor=="#111"||backgroundColor=="#111111") ic = "#222";
			var inner = this.inner = new zim.Circle(r*innerScale, ic, null, null, null, null, null, false);
			this.addChild(inner);

			if (!innerTicks) {
				var ic2 = "rgba(0,0,0,.1)";
				var inner2 = this.inner2 = new zim.Circle(r*(innerScale-.1), ic2, null, null, null, null, null, false);
				this.addChild(inner2);
			}
		}

		var stepsTotal = Math.abs(max - min) / step;
		if (useTicks && step != 0) {
			var ticks = this.ticks = new zim.Container({style:false});
			this.addChild(ticks);
			var tick;
			for (var i=0; i<(continuous?stepsTotal:stepsTotal+1); i++) {
				var tick = new zim.Rectangle(1, r*.2, tickColor, null, null, null, null, null, false);
				tick.regY = r * ((innerTicks) ? (innerScale-.05) : 1.28);
				tick.regX = .5;
				tick.rotation = (360 / (continuous?stepsTotal:stepsTotal+1)) * i;
				ticks.addChild(tick);
			}
		}

		this.setBounds(-r,-r,width,width);
		if (indicatorType == "dot" || indicatorType == "circle") {
			var indicator = this.indicator = new zim.Container({style:false});
			var indicatorShape = this.indicatorShape = new zim.Circle(r*.19, indicatorColor, null, null, null, null, null, false);
			indicator.addChild(indicatorShape);
			zim.sca(indicator, indicatorScale);
			indicator.regY = r - indicator.getBounds().width*indicatorScale/2 - r*.07;
		} else if (indicatorType == "line" || indicatorType == "rectangle") {
			var indicator = this.indicator = new zim.Container({style:false});
			var indicatorShape = this.indicatorShape = new zim.Rectangle(r * .1, r*.3, indicatorColor, null, null, null, null, null, null, false);
			indicator.addChild(indicatorShape);
			zim.sca(indicator, indicatorScale);
			indicator.regY = r - indicator.getBounds().width*indicatorScale/2 - r*.07;
			indicator.regX = r * .05;
		} else { // arrow
			var indicator = this.indicator = new zim.Container({style:false});
			var indicatorShape = this.indicatorShape = new zim.Triangle(r*.4, r*.4, r*.4, indicatorColor, null, null, null, null, null, null, false);
			indicator.addChild(indicatorShape);
			zim.sca(indicator, indicatorScale);
			indicator.regY = r - indicator.getBounds().height*indicatorScale*((innerTicks)?.85:.75);
			if (innerTicks) {
				indicatorShape.rotation = 180;
			}
		}
		indicator.regY /= indicatorScale;
		this.addChild(indicator);

		function snap(v) {
			if (step == 0) return v;
			return Math.round(v/step)*step;
		}

		var lastAngle;
		var startAngle;
		var moveEvent;
		var upEvent;
		var lastA = 0;
		var normalizedAmount; // amount within base rotation - based on min and max
		if (continuous) {
			var lastContinuous = 0;
			var continuousAngle = 0;
			var continuousBase = 0; // keeps track of 360s
			var continuousAdjust = false; // for hitting bounds
			var resetContinuous = false; // for adjusting lastContinuous on keyUp
			var resetContinuousCheck = true; // turned off by normal rotation but turned on by key and direct currentValue calls
		} else {
			normalizedAmount = min;
		}
		var touchID;
		this.on("mousedown", function(e) {
			stage = e.target.stage;
			if (that.zimAccessibility && that.zimAccessibility.aria) return;
			lastAngle = indicator.rotation;
			var p = that.parent.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
			var dX = p.x-that.x;
			var dY = that.y-p.y;
			startAngle = Math.atan2(dX,dY)*180/Math.PI;
			var pressTime = new Date().getTime();
			moveEvent = that.on("pressmove", function(e) {
				if (continuousAdjust) { // hit bounds so reset start
					var p = that.parent.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
					var dX = p.x-that.x;
					var dY = that.y-p.y;
					startAngle = Math.atan2(dX,dY)*180/Math.PI;
					lastAngle = indicator.rotation;
				}
				p = that.parent.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
				dX = p.x-that.x;
				dY = that.y-p.y;
				var endAngle = Math.atan2(dX,dY)*180/Math.PI;
				var angle = lastAngle + endAngle - startAngle;
				angle = (angle + 360*10000) % 360;
				if (limit && Math.abs(angle-lastA) > 180) return;
				setValue(angle);
				lastA = angle;
			});
			upEvent = this.on("pressup", function(e) {
				var deltaTime = new Date().getTime()-pressTime;
				if (deltaTime < 200) {
					p = that.parent.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
					dX = p.x-that.x;
					dY = that.y-p.y;
					var angle = Math.atan2(dX,dY)*180/Math.PI;
					setValue(angle);
				}
				lastAngle = indicator.rotation;
				that.off("pressmove", moveEvent);
				that.off("pressup", upEvent);
			});
		});

		function sign(n) {return n > 0 ? 1 : -1;}

		function setAccessibility() {
			if (that.zimAccessibility) that.zimAccessibility.changeTitle(that, null, true);
		}

		function setValue(angle) {
			if (continuous) {
				if (resetContinuous) { // if coming from keyup
					// lastContinuous = angle; // removed in 7.0.1
					resetContinuous = false;
				}
				if (angle > lastContinuous + 180) {
					continuousBase -= 360;
				} else if (angle < lastContinuous - 180) {
					continuousBase += 360;
				}
				continuousAngle  = continuousBase + angle;
				resetContinuousCheck = false;
				var earlierValue = that.currentValue;
				that.currentValue = snap(continuousAngle * (max-min) / 360);
				if (earlierValue != that.currentValue) that.dispatchEvent("change");
				lastContinuous = angle;
				resetContinuousCheck = true;
				return true;
			}
			var v; // value (not including min)
			if (angle < 0) angle += 360;
			angle = angle % 360;
			if (step != 0) {
				angle = Math.min(angle,  360 - 360 / (stepsTotal+1));
				v = snap(angle / (360 - 360 / (stepsTotal+1)) * (max - min));
				indicator.rotation = v * (360 - 360 / (stepsTotal+1)) / (max - min);
			} else {
				indicator.rotation = angle;
				v = (angle / 360) * (max - min);
			}
			if (v != lastValue) {
				lastValue = v;
				myValue = v + min;
				that.dispatchEvent("change");
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
			setAccessibility();
		}

		var myDampedValue;
		var dampObject;
		if (damp) {
			myDampedValue = min;
			dampObject = new zim.Damp(myDampedValue, damp);
			that.ticker = zim.Ticker.add(function () {
				myDampedValue = dampObject.convert(myValue);
			});
		}

		Object.defineProperty(this, 'currentValue', {
			get: function() {
				return damp?myDampedValue:myValue;
			},
			set: function(value) {
				if(zot(value)) return;
				if (continuous) {
					continuousAdjust = false;
					if (!zot(continuousMin) && !zot(continuousMax)) {
						if (continuousMin < continuousMax) {
							if (value < continuousMin) {value = continuousMin; continuousAdjust = true;}
							if (value > continuousMax) {value = continuousMax; continuousAdjust = true;}
						} else {
							if (value > continuousMin) {value = continuousMin; continuousAdjust = true;}
							if (value < continuousMax) {value = continuousMax; continuousAdjust = true;}
						}
					} else if (!zot(continuousMin)) {
						if (value < continuousMin) {value = continuousMin; continuousAdjust = true;}
					} else if (!zot(continuousMax)) {
						if (value > continuousMax) {value = continuousMax; continuousAdjust = true;}
					}
					if (resetContinuousCheck) {
						resetContinuous = true;
						continuousBase = Math.floor(value / (max-min)) * 360;
					}
				} else {
					if (min < max) {
						if (value < min) value = limit?min:max;
						if (value > max) value = limit?max:min;
					} else {
						if (value > min) value = limit?min:max;
						if (value < max) value = limit?max:min;
					}
				}
				myValue = value;
				value = snap(value);
				if (damp) damp.immediate(myValue);

				indicator.rotation = (value - min) * 360 / (max - min + (continuous?0:sign(max - min)*step));
				indicator.rotation = (indicator.rotation + 360 * 10000) % 360;
				lastValue = value - min;
				lastA = indicator.rotation;
				setAccessibility();
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		Object.defineProperty(this, 'currentValueEvent', {
			get: function() {
				return damp?myDampedValue:myValue;
			},
			set: function(value) {
				if (value != that.currentValue) {
					that.currentValue = value;
					that.dispatchEvent("change");
				}
			}
		});

		Object.defineProperty(this, 'min', {
			get: function() {
				return min;
			},
			set: function(value) {
				if (continuous) min = value;
				else if (zon) zog("min is read only");
			}
		});

		Object.defineProperty(this, 'max', {
			get: function() {
				return max;
			},
			set: function(value) {
				if (continuous) max = value;
				else if (zon) zog("max is read only");
			}
		});

		Object.defineProperty(this, 'continuous', {
			get: function() {
				return continuous;
			},
			set: function(value) {
				if (zon) zog("continuous is read only");
			}
		});

		Object.defineProperty(this, 'continuousMin', {
			get: function() {
				return continuousMin;
			},
			set: function(value) {
				continuousMin = value;
				if (that.currentValue < continuousMin) that.currentValue = continuousMin;
			}
		});

		Object.defineProperty(this, 'continuousMax', {
			get: function() {
				return continuousMax;
			},
			set: function(value) {
				continuousMax = value;
				if (that.currentValue > continuousMax) that.currentValue = continuousMax;
			}
		});

		Object.defineProperty(this, 'step', {
			get: function() {
				return step;
			},
			set: function(value) {
				if (zon) zog("step is read only");
			}
		});

		Object.defineProperty(this, 'keyArrowsH', {
			get: function() {
				return keyArrowsH;
			},
			set: function(value) {
				keyArrowsH = value;
			}
		});

		Object.defineProperty(this, 'keyArrowsV', {
			get: function() {
				return keyArrowsV;
			},
			set: function(value) {
				keyArrowsV = value;
			}
		});

		if (typeof KEYFOCUS !== typeof undefined) zim.KEYFOCUS = KEYFOCUS;
		Object.defineProperty(this, 'keyFocus', {
			get: function() {
				return zim.KEYFOCUS == that;
			},
			set: function(value) {
				zim.KEYFOCUS = that;
			}
		});
		if (keyArrows && !zim.KEYFOCUS) setFocus();
		this.on("mousedown", function() {if (keyArrows) setFocus()});
		function setFocus() {that.keyFocus = true; var d=document.activeElement; if (d) d.blur();}

		var leftCheck = false; var downCheck = false; var rightCheck = false; var upCheck = false;
		this.keyDownEvent = function(e) {
			if (!that.stage) return;
			if ((that.zimAccessibility && that.focus) || (!that.zimAccessibility && that.keyFocus)) {
				if (e.keyCode == 37 && keyArrowsH) leftCheck = true;
				else if (e.keyCode == 40 && keyArrowsV) downCheck = true;
				else if (e.keyCode == 39 && keyArrowsH) rightCheck = true;
				else if (e.keyCode == 38 && keyArrowsV) upCheck = true;
				if (that.keyInterval == null && (leftCheck || downCheck || rightCheck || upCheck)) {
					checkKey();
					// add traditional keydown delay
					that.keyTimeout = setTimeout(function() {
						if (that.keyInterval == null && (leftCheck || downCheck || rightCheck || upCheck)) that.keyInterval = setInterval(checkKey, 40);
					}, 140);
				}
			}
		}
		function checkKey() {
			if (leftCheck || downCheck) {
				if (step > 0) that.currentValueEvent -= step * sign(max-min);
				else that.currentValueEvent -= keyArrowsStep * sign(max-min);
			}
			if (rightCheck || upCheck) {
				if (step > 0) that.currentValueEvent += step * sign(max-min);
				else that.currentValueEvent += keyArrowsStep * sign(max-min);
			}
		}
		window.addEventListener("keydown", this.keyDownEvent);
		that.keyUpEvent = function(e) {
			if (e.keyCode == 37) leftCheck = false;
			else if (e.keyCode == 40) downCheck = false;
			else if (e.keyCode == 39) rightCheck = false;
			else if (e.keyCode == 38) upCheck = false;
			if (that.keyInterval != null && !leftCheck && !downCheck && !rightCheck && !upCheck) {
				clearInterval(that.keyInterval);
				that.keyInterval = null;
			}
		}
		window.addEventListener("keyup", this.keyUpEvent);

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
				if (value) {
					that.keyDownEvent = window.addEventListener("keydown", that.keyDownEvent);
					that.keyUpEvent = window.addEventListener("keyup", that.keyUpEvent);
				} else {
					window.removeEventListener("keydown", that.keyDownEvent);
					window.removeEventListener("keyup", that.keyUpEvent);
				}
			}
		});

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.Dial(min, max, step, width, backgroundColor, indicatorColor, indicatorScale, indicatorType, innerCircle, innerScale, useTicks, innerTicks, tickColor, limit, keyArrows, keyArrowsStep, keyArrowsH, keyArrowsV, continuous, continuousMin, continuousMax, damp, style, this.group, inherit));
		}

		this.dispose = function() {
			window.removeEventListener("keydown", that.keyDownEvent);
			window.removeEventListener("keyup", that.keyUpEvent);
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.Dial, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-63

//***************** RADIAL  64

/*--
zim.Tabs = function(width, height, tabs, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, vertical, spacing, currentEnabled, currentSelected, corner, base, keyEnabled, gradient, gloss, backing, rollBacking, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, backdropColor, align, valign, labelAlign, labelValign, labelIndent, labelIndentHorizontal, labelIndentVertical, indent, useTap, excludeCustomTap, style, group, inherit)

Tabs
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A traditional tab layout for along the edge of content.
Can also act as an independent button row or column.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var tabs = new Tabs({tabs:["A", "B", "C", "D"], spacing:5, corner:14});
tabs.center(stage);
tabs.on("change", function() {
	zog(tabs.selectedIndex);
	zog(tabs.text);
});
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 240) overall width of tab set (ZIM divides the width across tabs and spacing)
height - (default 60) height of tabs
tabs - (default ["1","2","3","4"]) an array of any String, Number, Label, Button, (or any DisplayObject)
	OR tab objects with the following properties available:
	any tab specific properties will override the default values from other parameters
	[{label:"String", width:200, backgroundColor:"red", rollBackgroundColor:"pink", selectedBackgroundColor:"grey", color:"yellow", selectedColor:"lime"}, {etc.}]
	label can be a String or a Label object - default text color is white
	Tab objects can also include wait properties for individual buttons.
	(this was put in place before Buttons were allowed in the tabs array - so you can just add a Button to the tab array instead)
	See wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal and waitEnabled parameters below
	wait can be used with button's waiting property to offer an alternative to a loading screen or confirmation panel
	also see the button's clearWait() method to cancel the wait state and waited event that triggers when the wait is done
	wait will primarily be applicable when the tabs are used as a set of buttons rather than traditional tabbing
	Warning - do not use the same array for multiple tabs as the array is turned into an array of objects used by the Tabs object.
backgroundColor - (default "#777") the background color of a deselected tab when not rolled over
rollBackgroundColor - (default "#555") the rollover background color
selectedBackgroundColor - (default "#333") the background color of the selected tab (any CSS color)
selectedRollBackgroundColor - (default rollBackgroundColor) the background color of the selected tab on rollover (if currentEnabled is true)
color - (default "white") the text color of a deselected tab when not rolled over
rollColor - (default color) the rollover color (selected tabs do not roll over)
selectedColor - (default color) the text color of the selected tab (any CSS color)
selectedRollColor - (default rollColor) the text color of the selected tab on rollover (if currentEnabled is true)
vertical - (default false) set to true to make vertical tabs with text still horizontal
spacing - (default 1) is the pixels between tab buttons
currentEnabled - (default false) set to true to be able to press (a second time) the selected tab button
currentSelected - (default true) set to false to not highlight the current button (good for button bars)
	setting this to true will set currentEnabled to true
corner - (default 0) the corner radius of the tabs
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
base - (default "none") specifiy a side for flat bottom when corner is set (but not set to an array)
	other values are "bottom" (default when corner and not vertical), "left" (default when corner and vertical), "top", "right"
	** this was flatBottom - but then vertical tabs were added so it was changed in ZIM 9.2.0
keyEnabled - (default true) so tab key cycles through tabs, shift tab backwards
gradient - (default null) 0 to 1 (try .3) adds a gradient to the tabs
gloss - (default null) 0 to 1 (try .1) adds a gloss to the tabs
wait - (default null) String text for tab to say when pressed to enter a wait mode
	The wait parameters can be (and probably will be) set as properties for each individual tab in the tabs array
waitTime - (default 20000) milliseconds to stay in wait state before returning to normal tab
waitBackgroundColor - (default color) the color of the tab during wait period
rollWaitBackgroundColor - (default color) the color of the tab during wait period
waitBackgroundColor - (default red) color to make button during wait time if wait is set
rollWaitBackgroundColor - (default rollColor) color for button when waiting and rolled over
waitColor - (default label's color) color to make text during wait time if wait is set
rollWaitColor - (default label's roll color) color for text when waiting and rolled over
waitModal - (default false) set to true to exit wait state if user clicks off tabs or to another tab
waitEnabled - (default true) set to false to disable tabs while in wait mode
backdropColor - (default null) set to a color to show behind the tabs (handy for when corner is not 0)
align - (default "center") horizontal align
valign - (default "center") vertical align
labelAlign - (default "center") horizontal align of the label only
labelValign - (default "center") vertical align of the label only
labelIndent - (default indent) indent of label when align or valign is set - usually same as indent unless custom objects are in tabs
labelIndentHorizontal - (default indent) horizontal indent of label when align or valign is set
labelIndentVertical - (default indent) vertical indent of label when align or valign is set
indent - (default 10) indent of items when align or valign is set and there are custom objects in tabs
useTap - (default false) set to true to use tap to activate otherwise uses ACTIONEVENT (mousedown or click)
excludeCustomTap - (default false) set to true to exclude custom buttons from tap() which would override existing tap() on the buttons
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
addAt(items, index, setStyle) - an array of items to insert at an index in the tab - tabs will grow in size - returns the object for chaining
	To keep the same size - run insertAt() and then remake the Tabs using the tabs.buttons array as the tabs parameter
	Can also send in a setStyle object literal {} with color, rollColor, selectedColor and selectedRollColor plus the background color versions of these!
removeAt(index, number) - remove a tab index an number of items (default 1) - tabs will shrink in size - returns the object for chaining
first() - select first tab - returns object to chain
last() - select last tab - returns object to chain
getItemIndex(item) - gets the index of the list item provided

hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
selectedIndex - gets or sets the selected index of the tabs
selected - gets the selected button - selected.enabled = true, etc.
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
tabs - gets or sets tabs object (will have to manually change buttons as well as adjust props)
backgroundColor - gets or sets default unselected background color - not applied to custom buttons
rollBackgroundColor - gets or sets default rolled over background color - not applied to custom buttons
selectedBackgroundColor - gets or sets default selected background color - not applied to custom buttons
selectedRollBackgroundColor - gets or sets default selected roll background color - not applied to custom buttons
color - gets or sets default unselected text color - not applied to custom buttons
rollColor - gets or sets default rolled over text color - not applied to custom buttons
selectedColor - gets or sets default selected text color - not applied to custom buttons
selectedRollColor - gets or sets default selected roll text color - not applied to custom buttons
text - gets current selected label text
label - gets current selected label object
buttons - an array of the ZIM Button objects. buttons[0].enabled = false;
labels - an array of the ZIM Label objects. labels[0].text = "YUM"; labels[2].y -= 10;
buttonDown - the button that is currently being pressed
backdrop - reference to backdrop Rectangle if backdropColor is provided
keyEnabled - gets or sets whether the tab key and shift tab key cycles through tabs (does not affect accessibility)
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
keyFocus - get or set the keyboard focus on the component - see also zim.KEYFOCUS
	will be set to true if this component is the first made or component is the last to be used
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties
and stage.update() in change event to see component change its graphics

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)

EVENTS
dispatches a "change" event when a tab changes (but not when setting selectedIndex property)

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+65
	zim.Tabs = function(width, height, tabs, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, vertical, spacing, currentEnabled, currentSelected, corner, base, keyEnabled, gradient, gloss, backing, rollBacking, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, backdropColor, align, valign, labelAlign, labelValign, labelIndent, labelIndentHorizontal, labelIndentVertical, indent, useTap, excludeCustomTap, style, group, inherit) {
		var sig = "width, height, tabs, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, vertical, spacing, currentEnabled, currentSelected, corner, base, keyEnabled, gradient, gloss, backing, rollBacking, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, backdropColor, align, valign, labelAlign, labelValign, labelIndent, labelIndentHorizontal, labelIndentVertical, indent, useTap, excludeCustomTap, style, group, inherit";
		var duo; if (duo = zob(zim.Tabs, arguments, sig, this)) return duo;
		z_d("65");

		var DS = style===false?{}:zim.getStyle("Tabs", this.group, inherit);
		if (zot(vertical)) vertical = DS.vertical!=null?DS.vertical:false;
		var specifiedWidth = !zot(width);

		if (zot(width)) width = DS.width!=null?DS.width:(vertical?60:240);
		if (zot(height)) height = DS.height!=null?DS.height:(vertical?240:60);

		this.zimContainer_constructor(null,null,null,null,true);
		this.type = "Tabs";
		this.group = group;

		if (zot(tabs) || tabs.length<=0) tabs = DS.tabs!=null?DS.tabs:[1,2,3,4];
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"#777";
		if (zot(rollBackgroundColor)) rollBackgroundColor = DS.rollBackgroundColor!=null?DS.rollBackgroundColor:"#555";
		if (zot(selectedBackgroundColor)) selectedBackgroundColor = DS.selectedBackgroundColor!=null?DS.selectedBackgroundColor:"#333";
		if (zot(selectedRollBackgroundColor)) selectedRollBackgroundColor = DS.selectedRollBackgroundColor!=null?DS.selectedRollBackgroundColor:rollBackgroundColor;
		if (zot(color)) color = DS.color!=null?DS.color:"white";
		if (zot(rollColor)) rollColor = DS.rollColor!=null?DS.rollColor:color;
		if (zot(selectedColor)) selectedColor = DS.selectedColor!=null?DS.selectedColor:color;
		if (zot(selectedRollColor)) selectedRollColor = DS.selectedRollColor!=null?DS.selectedRollColor:rollColor;
		if (zot(backing)) backing = DS.backing!=null?DS.backing.clone():null;
		if (zot(rollBacking)) rollBacking = DS.rollBacking!=null?DS.rollBacking.clone():null;
		if (zot(align)) align=DS.align!=null?DS.align:"center";
		if (zot(valign)) valign=DS.valign!=null?DS.valign:"center";
		if (zot(labelAlign)) labelAlign=DS.labelAlign!=null?DS.labelAlign:align;
		if (zot(labelValign)) labelValign=DS.labelValign!=null?DS.labelValign:valign;

		if (zot(indent)) indent=DS.indent!=null?DS.indent:10;
		if (zot(labelIndent)) labelIndent=DS.labelIndent!=null?DS.labelIndent:indent;
		if (zot(labelIndentHorizontal)) labelIndentHorizontal=DS.labelIndentHorizontal!=null?DS.labelIndentHorizontal:labelIndent;
		if (zot(labelIndentVertical)) labelIndentVertical=DS.labelIndentVertical!=null?DS.labelIndentVertical:labelIndent;

		if (zot(currentEnabled)) currentEnabled = DS.currentEnabled!=null?DS.currentEnabled:false;
		if (zot(currentSelected)) {
			currentSelected = DS.currentSelected!=null?DS.currentSelected:true; // keep the highlight on after pressup
		}
		if (!currentSelected) currentEnabled = true; // button bar

		if (zot(spacing)) spacing = DS.spacing!=null?DS.spacing:1;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:0;
		if (zot(gradient)) gradient = DS.gradient!=null?DS.gradient:null;
		if (zot(gloss)) gloss = DS.gloss!=null?DS.gloss:null;
		if (zot(base)) base = DS.base!=null?DS.base:((corner==0&&!Array.isArray(corner))?"none":(vertical?"left":"bottom"));
		if (zot(keyEnabled)) keyEnabled = DS.keyEnabled!=null?DS.keyEnabled:true;
		if (zot(useTap)) useTap = DS.useTap!=null?DS.useTap:false;
		if (zot(excludeCustomTap)) excludeCustomTap = DS.excludeCustomTap!=null?DS.excludeCustomTap:false;
		if (zot(backdropColor)) backdropColor = DS.backdropColor!=null?DS.backdropColor:null;
		if (base != "none" && corner!=0 && !Array.isArray(corner)) {
			switch(base) {
			    case "bottom":
			        corner = [corner, corner, 0, 0]; break;
			    case "left":
			        corner = [0, corner, corner, 0]; break;
				case "top":
			        corner = [0, 0, corner, corner]; break;
				case "right":
 			        corner = [corner, 0, 0, corner]; break;
			}
		} else if (!Array.isArray(corner)) {
			corner = [corner, corner, corner, corner];
		}


		var that = this;
		this.keyEnabled = keyEnabled;

		var myIndex = 0; // local value for this.selectedIndex
		var labels = []
		var buttons = [];
		var button; var t;
		var num = tabs.length;
		var tabW = (width - spacing*(num-1))/num;
		var tabH = (height - spacing*(num-1))/num;

		if (!zot(backdropColor)) {
			// may be resized later
			var backdrop = this.backdrop = new zim.Rectangle(width,height,backdropColor, null, null, null, null, null, false);
			this.addChild(backdrop);
		}

		// Make a list be tab data objects if string, number, label or {} - other objects are ignored
		var mix = false; // used to determine how to treat indent
		function makeDataObject(list) {
			var mixType;
			for (var i=0; i<list.length; i++) {
				var item = list[i];
				if (item.constructor === {}.constructor) {
					if (mixType && mixType != "TabObject") mix = true;
					mixType = "TabObject";
					item.type = "TabObject";
					continue;
				}
				if (typeof item == "string" || typeof item == "number" || item.type == "Label") {
					list[i] = {type:"TabObject", label:(String((item!=null))||item.type=="Label")?item:"1"};
					if (mixType && mixType != "TabObject") mix = true;
					mixType = "TabObject";
				} else {
					if (mixType && mixType == "TabObject") mix = true;
					mixType = "Other";
				}
			}
		}
		makeDataObject(tabs);

		// calculate widths - done only once at start
		var total = 0; var t;
		var newTabW; var nonSpecifiedCount = 0;
		for (var i=0; i<tabs.length; i++) {
			t = tabs[i];
			if (!vertical) {
				if (zot(t.width)) nonSpecifiedCount++;
				total += (zot(t.width))?tabW:t.width;
			} else if (specifiedWidth) {
				t.width = t.width?t.width:width;
			}
		}
		if (!vertical) {
			if (total > width - spacing*(num-1)) {
				// go back and assign proportional widths
				for (i=0; i<tabs.length; i++) {
					t = tabs[i];
					t.width = (width - spacing*(num-1)) / total * ((zot(t.width))?tabW:t.width);
				}
			} else if (Math.round(total) < Math.round(width - spacing*(num-1))) {
				// go back and readjust the average of non specified widths
				if (nonSpecifiedCount > 0) {
					newTabW = (num*tabW-(total-nonSpecifiedCount*tabW))/nonSpecifiedCount;
					for (i=0; i<tabs.length; i++) {
						t = tabs[i];
						t.width = ((zot(t.width))?newTabW:t.width);
					}
				} else {
					// that.width = width;
					// // if (zon) zog("ZIM Tabs - total less than width");
					// width = total + spacing*(num-1);
				}
			}
		}

		// Create Label Objects including Label and also get max width
		function makeLabels(list, addStyle) {
			for (i=0; i<list.length; i++) {
				t = list[i];
				if (t == "") continue; // was break?
				if (t.type == "TabObject") {
					if (zot(t.label)) t.label = " ";
					if (typeof t.label == "string" || typeof t.label == "number") {
						// will do colors in Button
						t.label = new zim.Label({
							text:t.label,
							size:DS.size!=null?DS.size:(vertical?tabH/2:height/2),
							font:DS.font!=null?DS.font:null,
							style:false,
							inherit:addStyle
						});
					}
				}
			}
		}
		makeLabels(tabs);


		// get maxWidth - done only once at start
		var maxWidth = 0;
		for (i=0; i<tabs.length; i++) {
			t = tabs[i];
			if (!t.width && vertical && t.label.width > maxWidth) maxWidth = t.label.width;
		}

		// create buttons
		var hMaxWidth = 0;
		function makeButtons(list, addStyle) {
			var buttons = [];
			var labels = [];
			for (i=0; i<list.length; i++) {
				t = list[i];
				if (t.type == "TabObject") {
					var tabInfo = {
						color:zot(t.color)?color:t.color,
						rollColor:zot(t.rollColor)?rollColor:t.rollColor,
						selectedColor:zot(t.selectedColor)?selectedColor:t.selectedColor,
						selectedRollColor:zot(t.selectedRollColor)?selectedRollColor:t.selectedRollColor,
						backgroundColor:zot(t.backgroundColor)?backgroundColor:t.backgroundColor,
						rollBackgroundColor:(zot(t.rollBackgroundColor))?rollBackgroundColor:t.rollBackgroundColor,
						selectedBackgroundColor:(zot(t.selectedBackgroundColor))?selectedBackgroundColor:t.selectedBackgroundColor,
						selectedRollBackgroundColor:(zot(t.selectedRollBackgroundColor))?selectedRollBackgroundColor:t.selectedRollBackgroundColor,
						wait:(zot(t.wait))?wait:t.wait
					};
					if (addStyle) {
						for (aStyle in addStyle) {
							if (!zot(addStyle[aStyle])) tabInfo[aStyle] = addStyle[aStyle];
						}
					}
					button = new zim.Button({
						width:vertical?(zot(t.width)?specifiedWidth?width:(maxWidth+tabH/2+corner[0]/2):t.width):((zot(t.width))?tabW:t.width),
						height:vertical?tabH:height,
						label:t.label,
						borderColor:DS.borderColor!=null?DS.borderColor:null,
						borderWidth:DS.borderWidth!=null?DS.borderWidth:null,
						backgroundColor:tabInfo.backgroundColor,
						rollBackgroundColor:tabInfo.rollBackgroundColor,
						corner:corner,
						shadowColor:-1,
						gradient:gradient,
						gloss:gloss,
						backing:backing?backing.clone():null,
						rollBacking:rollBacking?rollBacking.clone():null,
						wait:tabInfo.wait,
						waitTime:(zot(t.waitTime))?waitTime:t.waitTime,
						waitBackgroundColor:(zot(t.waitBackgroundColor))?waitBackgroundColor:t.waitBackgroundColor,
						rollWaitBackgroundColor:(zot(t.rollWaitBackgroundColor))?rollWaitBackgroundColor:t.rollWaitBackgroundColor,
						waitColor:(zot(t.waitColor))?waitColor:t.waitColor,
						rollWaitColor:(zot(t.rollWaitBackgroundColor))?rollWaitColor:t.rollWaitColor,
						waitModal:(zot(t.waitModal))?waitModal:t.waitModal,
						waitEnabled:(zot(t.waitEnabled))?waitEnabled:t.waitEnabled,
						align:labelAlign,
						valign:labelValign,
						indentHorizontal:labelIndentHorizontal,
						indentVertical:labelIndentVertical,
						inherit:DS
					});
					button.tabInfo = tabInfo;
					// set the label colors
					button.color = button.tabInfo.color;
					button.rollColor = button.tabInfo.rollColor;
					button.type = "TabsButton";
					button.listIndentReady = true;
				} else {

					if (t.listIndentReady) button = t;
					else button = new zim.Container();
					button.content = t;
					if (t.checkBox) button.checkBox = t.checkBox;
					button.tabInfo = {};
					if (excludeCustomTap) button.excludeTap = true;
					button.tabInfo.color = zot(t.color)?color:t.color;
					button.tabInfo.backgroundColor = zot(t.backgroundColor)?backgroundColor:t.backgroundColor;

					if (!t.listIndentReady) {
						new zim.Rectangle(
							Math.max(t.width, vertical?(specifiedWidth?width:(maxWidth+tabH/2+corner[0]/2)):tabW),
							Math.max(t.height, vertical?tabH:height),
							zim.faint
						).addTo(button);
						t.center(button);
						if (!(align=="center" || align=="middle")) t.pos(0,null,align=="right");
						if (!(valign=="center" || valign=="middle")) t.pos(null,0,null,valign=="bottom");
					}
				}
				if (button.width > hMaxWidth) hMaxWidth = button.width;

				// apply events
				if (useTap) {
					button.tap(function (e) {
						change(e.currentTarget.znum);
						that.dispatchEvent("change");
						if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
					})
				} else {
					button.zimTabEvent = button.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", function(e) {
						change(e.currentTarget.znum);
						that.dispatchEvent("change");
						if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
					});
				}
				button.on("mousedown", function(e) {
					that.buttonDown = e.currentTarget;
				});
				button.on("pressup", function() {
					that.buttonDown = null;
				});
				labels.push(t.label);
				buttons.push(button);
			}
			return [buttons, labels];
		}
		var result = makeButtons(tabs);
		buttons = result[0];
		labels = result[1];


		// finalize index, location, bounds
		function prepareAllButtons() {
			var lastX = 0; var lastY = 0; var t; var button;
			for (i=0; i<buttons.length; i++) {
				button = buttons[i];
				button.znum = i;
				button.selectedIndex = i;
				if (button.selectedIndex == myIndex && currentSelected) {
					button.backgroundColor = selectedBackgroundColor;
					button.rollBackgroundColor = selectedRollBackgroundColor;
					button.color = selectedColor;
				}
				if (labels[i]) labels[i].znum = i;
				that.addChild(button);
				if (!vertical || specifiedWidth) {
					button.x = (width-button.width)/2;
					button.y = (height-button.height)/2;
				}
				if (vertical) {
					button.y = lastY;
					lastY = button.y + button.height + spacing;
					if (align=="left") button.x = (button.type=="TabsButton" && !mix?0:indent) + (button.regX - (button.getBounds()?button.getBounds().x:0))*button.scaleX;
					else if (align=="right") button.x = (specifiedWidth?width:hMaxWidth)-button.width-(button.type=="TabsButton" && !mix?0:indent);
				} else {
					button.x = lastX;
					lastX = button.x + button.width + spacing;
					if (valign=="top") button.y = (button.type=="TabsButton" && !mix?0:indent) + (button.regY - (button.getBounds()?button.getBounds().y:0))*button.scaleY;
					else if (valign=="bottom") button.y = height-button.height-(button.type=="TabsButton" && !mix?0:indent);
				}
				if (i==0 && !currentEnabled) button.enabled = false;
				button.enabled = true;
			}

			// might have to use w and h rather than width and height if run multiple times...
			if (!zot(backdropColor)) {
				that.removeChild(backdrop);
			}

			that._bounds = null; // for old version of CreateJS including Animate's
			// that.setBounds();
			var bou = that.getBounds();

			if (!bou && createjs.EaselJS.version[0] != 0) {
				w = h = 0;
				that.setBounds();
			} else {
				if (bou) {
					w = vertical&&bou.width!=0&&specifiedWidth?width:bou.width;
					h = vertical&&bou.height!=0?bou.height:height;
					that.setBounds(w,h);
				}
			}
			if (!zot(backdropColor)) {
				backdrop.widthOnly = w;
				backdrop.heightOnly = h;
				that.addChildAt(backdrop,0);
			}

			if (vertical && !specifiedWidth) {
				for (i=0; i<=buttons.length; i++) {
					if (align=="center" || align=="middle") button.x = (w-button.width)/2;
				}
			}
		}
		prepareAllButtons(); // operates on buttons array - not tabs array

		function change(num) {
			var b = buttons[myIndex];
			if (b && currentSelected) {
				if (b.tabInfo && zot(b.tabInfo.wait)) {
					if (b.tabInfo.backgroundColor) b.backgroundColor = b.tabInfo.backgroundColor;
					if (b.tabInfo.rollBackgroundColor) b.rollBackgroundColor = b.tabInfo.rollBackgroundColor;
					if (b.tabInfo.color && b.label) b.color = b.tabInfo.color;
					if (b.tabInfo.rollColor && b.label) b.rollColor = b.tabInfo.rollColor;
				}
				if (!currentEnabled) b.enabled = true;
			}
			myIndex = num;
			b = buttons[myIndex];
			if (b) {
				if (b.tabInfo && zot(b.tabInfo.wait) && currentSelected) {
					if (b.tabInfo.selectedBackgroundColor) b.backgroundColor = b.tabInfo.selectedBackgroundColor;
					if (b.tabInfo.selectedRollBackgroundColor) b.rollBackgroundColor = b.tabInfo.selectedRollBackgroundColor;
					if (b.tabInfo.selectedColor && b.label) b.color = b.tabInfo.selectedColor;
					if (b.tabInfo.selectedRollColor && b.label) b.rollColor = b.tabInfo.selectedRollColor;
				}
				if (!currentEnabled) b.enabled = false;
			}
		}

		this.addAt = function(items, index, addStyle) {
			if (zot(index)) index = buttons.length;
			index = zim.constrain(index, 0, buttons.length);
			// create tabObjects from new item(s)
			if (!Array.isArray(items)) items = [items];
			makeDataObject(items);
			makeLabels(items, addStyle);
			var result = makeButtons(items, addStyle);
			var buts = result[0];
			var labs = result[1];
			if (index <= myIndex) myIndex += buts.length;
			// insert buttons into buttons and labels into labels
			var args = [index, 0].concat(buts);
			Array.prototype.splice.apply(buttons, args);
			args = [index, 0].concat(labs);
			Array.prototype.splice.apply(labels, args);
			prepareAllButtons(); // operates on buttons array - not tabs array
			return that;
		}

		this.removeAt = function(num, index) {
			if (buttons.length == 0) return that;
			if (zot(num)) num = 1;
			if (zot(index)) index = buttons.length-num;
			index = zim.constrain(index, 0, buttons.length-num);

			// adjust selection
			if (myIndex >= index && myIndex <= index+num) myIndex = -1;
			if (myIndex > index+num) myIndex -= num;

			// remove listeners
			for (i=index; i<index+num; i++) {
				if (buttons[i].tabInfo) {
					if (buttons[i].tabInfo.backgroundColor) buttons[i].backgroundColor = buttons[i].tabInfo.backgroundColor;
					if (buttons[i].tabInfo.color && buttons[i].label) buttons[i].color = buttons[i].tabInfo.color;
				}

				if (useTap) buttons[i].noTap();
				else buttons[i].off((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", buttons[i].zimTabEvent);
				buttons[i].removeFrom();
			}
			// splice from buttons and labels from labels
			buttons.splice(index, num);
			labels.splice(index, num);
			prepareAllButtons(); // operates on buttons array - not tabs array
			return that;
		}

		window.addEventListener("keydown", function(e) {
			if (!that.keyEnabled || !that.keyFocus || that.zimAccessibility) return;
			if (e.keyCode == 9) {
				var next = myIndex; // note that change updates the index
				if (e.shiftKey) {
					change((--next<0)?tabs.length-1:next);
				} else {
					change((++next>tabs.length-1)?0:next);
				}
				that.dispatchEvent("change");
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				e.preventDefault();
			}
		});

		Object.defineProperty(this, 'selected', {
			get: function() {
				return buttons[myIndex];
			},
			set: function(value) {
				if (zon) zog("selected is read only - try selectedIndex");
			}
		});

		Object.defineProperty(this, 'selectedIndex', {
			get: function() {
				return myIndex;
			},
			set: function(value) {
				// change(Math.min(Math.max(value, 0), tabs.length-1));
				change(value);
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		this.last = function() {
			this.selectedIndex = this.buttons.length-1;
			return this;
		}
		this.first = function() {
			this.selectedIndex = 0;
			return this;
		}

		Object.defineProperty(this, 'tabs', {
			get: function() {
				return myIndex;
			},
			set: function(value) {
				change(Math.min(Math.max(value, 0), tabs.length-1));
				if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
		});

		Object.defineProperty(this, 'color', {
			get: function() {
				if (that.buttons[0].tabItems) return that.buttons[0].tabItems.color;
				else return color;
			},
			set: function(value) {
				color = value;
				if (that.buttons[0].tabInfo) {
					for (var i=0; i<that.buttons.length; i++) {
						if (that.buttons[i].tabInfo) {
							that.buttons[i].tabInfo.color = value;
						}
						if (i != myIndex || !currentSelected) that.buttons[i].color = color;
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
			}
		});

		Object.defineProperty(this, 'rollColor', {
			get: function() {
				if (that.buttons[0].tabItems) return that.buttons[0].tabItems.rollColor;
				else return rollColor;
			},
			set: function(value) {
				rollColor = value;
				if (that.buttons[0].tabInfo) {
					for (var i=0; i<that.buttons.length; i++) {
						if (that.buttons[i].tabInfo) {
							that.buttons[i].tabInfo.rollColor = value;
						}
						that.buttons[i].rollColor = rollColor;
						if (i != myIndex && that.buttons[i].rolled) that.buttons[i].color = rollColor;
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
			}
		});

		Object.defineProperty(this, 'selectedColor', {
			get: function() {
				if (that.buttons[0].tabItems) return that.buttons[0].tabItems.selectedColor;
				else return selectedColor;
			},
			set: function(value) {
				selectedColor = value;
				if (that.buttons[0].tabInfo) {
					for (var i=0; i<that.buttons.length; i++) {
						if (that.buttons[i].tabInfo) {
							that.buttons[i].tabInfo.selectedColor = value;
						}
						if ((i == myIndex && currentSelected)) that.buttons[i].color = selectedColor;
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
			}
		});

		Object.defineProperty(this, 'selectedRollColor', {
			get: function() {
				if (that.buttons[0].tabItems) return that.buttons[0].tabItems.selectedRollColor;
				else return selectedRollColor;
			},
			set: function(value) {
				selectedRollColor = value;
				if (that.buttons[0].tabInfo) {
					for (var i=0; i<that.buttons.length; i++) {
						if (that.buttons[i].tabInfo) {
							that.buttons[i].tabInfo.selectedRollColor = value;
						}
						if ((i == myIndex && currentSelected) && that.buttons[i].rolled) that.buttons[i].color = selectedRollColor;
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
			}
		});

		Object.defineProperty(this, 'backgroundColor', {
			get: function() {
				if (that.buttons[0].tabItems) return that.buttons[0].tabItems.backgroundColor;
				else return backgroundColor;
			},
			set: function(value) {
				backgroundColor = value;
				if (that.buttons[0].tabInfo) {
					for (var i=0; i<that.buttons.length; i++) {
						if (that.buttons[i].tabInfo) {
							that.buttons[i].tabInfo.backgroundColor = value;
						}
						if (i != myIndex || !currentSelected) that.buttons[i].backgroundColor = backgroundColor;
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
			}
		});

		Object.defineProperty(this, 'rollBackgroundColor', {
			get: function() {
				if (that.buttons[0].tabItems) return that.buttons[0].tabItems.rollBackgroundColor;
				else return rollBackgroundColor;
			},
			set: function(value) {
				rollBackgroundColor = value;
				if (that.buttons[0].tabInfo) {
					for (var i=0; i<that.buttons.length; i++) {
						if (that.buttons[i].tabInfo) {
							that.buttons[i].tabInfo.rollBackgroundColor = value;
						}
						that.buttons[i].rollBackgroundColor = rollBackgroundColor;
						if (i != myIndex && that.buttons[i].rolled) that.buttons[i].backgroundColor = rollBackgroundColor;
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
			}
		});

		Object.defineProperty(this, 'selectedBackgroundColor', {
			get: function() {
				if (that.buttons[0].tabItems) return that.buttons[0].tabItems.selectedBackgroundColor;
				else return selectedBackgroundColor;
			},
			set: function(value) {
				selectedBackgroundColor = value;
				if (that.buttons[0].tabInfo) {
					for (var i=0; i<that.buttons.length; i++) {
						if (that.buttons[i].tabInfo) {
							that.buttons[i].tabInfo.selectedBackgroundColor = value;
						}
						if ((i == myIndex && currentSelected)) that.buttons[i].backgroundColor = selectedBackgroundColor;
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
			}
		});

		Object.defineProperty(this, 'selectedRollBackgroundColor', {
			get: function() {
				if (that.buttons[0].tabItems) return that.buttons[0].tabItems.selectedRollBackgroundColor;
				else return selectedRollBackgroundColor;
			},
			set: function(value) {
				selectedRollBackgroundColor = value;
				if (that.buttons[0].tabInfo) {
					for (var i=0; i<that.buttons.length; i++) {
						if (that.buttons[i].tabInfo) {
							that.buttons[i].tabInfo.selectedRollBackgroundColor = value;
						}
						if ((i == myIndex && currentSelected) && that.buttons[i].rolled) that.buttons[i].backgroundColor = selectedRollBackgroundColor;
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
			}
		});

		Object.defineProperty(this, 'label', {
			get: function() {
				return labels[myIndex];
			},
			set: function(value) {
				if (zon) zog("selected is read only - try selectedIndex");
			}
		});

		Object.defineProperty(this, 'text', {
			get: function() {
				return (labels[myIndex]!=null) ? labels[myIndex].text : undefined;
			},
			set: function(value) {
				if (zon) zog("selected is read only - try selectedIndex");
			}
		});

		Object.defineProperty(this, 'buttons', {
			get: function() {
				return buttons;
			},
			set: function(value) {
				if (zon) zog("buttons is read only");
			}
		});

		Object.defineProperty(this, 'labels', {
			get: function() {
				return labels;
			},
			set: function(value) {
				if (zon) zog("labels is read only");
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
			}
		});

		if (typeof KEYFOCUS !== typeof undefined) zim.KEYFOCUS = KEYFOCUS;
		Object.defineProperty(this, 'keyFocus', {
			get: function() {
				return zim.KEYFOCUS == that;
			},
			set: function(value) {
				zim.KEYFOCUS = that;
			}
		});
		if (keyEnabled && zim.KEYFOCUS) setFocus();
		this.on("mousedown", function() {if (keyEnabled) setFocus()});
		function setFocus() {that.keyFocus = true; var d=document.activeElement; if (d) d.blur();}

		var bou = that.getBounds();
		if (zot(bou) || bou.width==0) that.setBounds(0,0,width,height);
		else that.width = width;

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			var tabsCopy = zim.copy(tabs, true);
			for (var i=0; i<tabsCopy.length; i++) {
				tabsCopy[i].label = tabsCopy[i].label.clone();
			}
			return that.cloneProps(new zim.Tabs(width, height, tabsCopy, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, vertical, spacing, currentEnabled, currentSelected, corner, base, keyEnabled, gradient, gloss, backing, rollBacking, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, backdropColor, align, valign, labelAlign, labelValign, labelIndent, labelIndentHorizontal, labelIndentVertical, indent, useTap, excludeCustomTap, style, this.group, inherit));
		}
	}
	zim.extend(zim.Tabs, zim.Container, "clone", "zimContainer", false);
	//-65

/*--
zim.Pad = function(width, cols, rows, keys, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, spacing, currentEnabled, corner, labelColor, gradient, gloss, backing, rollBacking, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, style, group, inherit)

Pad
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A pad that has rows and cols made of square keys.
When the keys are pressed the pad will dispatch a change event - get the selectedIndex or text property.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var pad = new Pad();
pad.center(stage);
pad.on("change", function() {
	zog(pad.selectedIndex); // 0-8
	zog(pad.text); // 1-9
});
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
** inherits STYLE from type selector for Pad, then general styles then type selector for Tabs
width - (default 150) overall width of pad (ZIM divides the width across cols and spacing)
cols - (default 3) the columns in the pad
rows - (default cols) the rows in the pad
keys - (default an Array for cols x rows) an array of key objects with the following properties available:
	any key specific properties will override the default values from other parameters
	[{label:"String", width:200, backgroundColor:"Red", rollBackgroundColor:"pink", selectedBackgroundColor:"grey"}, {etc.}]
	the label can be a String or a Label object - default text color is white
	Key objects can also include wait properties for individual buttons.
	See wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal and waitEnabled parameters below
	wait can be used with button's waiting property to offer an alternative to a loading screen or confirmation panel
	also see the button's clearWait() method to cancel the wait state and waited event that triggers when the wait is done
backgroundColor - (default "#777") the background color of a deselected key when not rolled over
rollBackgroundColor - (default "#555") the rollover background color (selected keys do not roll over)
selectedBackgroundColor - (default "#333") the background color of the selected key (any CSS color)
color - (default "white") the text color of a deselected key when not rolled over
selectedColor - (default color) the text color of the selected key (any CSS color)
rollColor - (default color) the rollover color (selected keys do not roll over)
spacing - (default 1) is the pixels between key buttons
currentEnabled - (default true) set to false to make selected key not pressable
corner - (default 0) the corner radius of the keys
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
labelColor - (default "white") the color of the label
gradient - (default null) 0 to 1 (try .3) adds a gradient to the tabs
gloss - (default null) 0 to 1 (try .1) adds a gloss to the tabs
wait - (default null) String text for button to say when pressed to enter a wait mode
	The wait parameters can be (and probably will be) set as properties for each individual button in the pads array
waitTime - (default 20000) milliseconds to stay in wait state before returning to normal button
waitBackgroundColor - (default color) the color of the button during wait period
rollWaitBackgroundColor - (default color) the color of the button during wait period
waitBackgroundColor - (default red) color to make button during wait time if wait is set
rollWaitBackgroundColor - (default rollColor) color for button when waiting and rolled over
waitColor - (default label's color) color to make text during wait time if wait is set
rollWaitColor - (default label's roll color) color for text when waiting and rolled over
waitModal - (default false) set to true to exit wait state if user clicks off the pad or to another button
waitEnabled - (default true) set to false to disable pad while in wait mode
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
selectedIndex - gets or sets the selected index of the pad
text - gets current selected label text
selected - gets the selected button - selected.enabled = true, etc.
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
label - gets current selected label object
selectedBackgroundColor - gets or sets default selected background color
backgroundColor - gets or sets default unselected background color
rollBackgroundColor - gets or sets default rolled over background color
color - gets or sets default unselected text color
rollColor - gets or sets default rolled over text color
selectedColor - gets or sets default selected text color
buttons - an array of the ZIM Button objects. buttons[0].enabled = false;
labels - an array of the ZIM Label objects. labels[0].text = "YUM"; labels[2].y -= 10;
tabs - an array of the zim Tabs objects (one object per row)
enabled - default is true - set to false to disable
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

OPTIMIZED
This component is affected by the general OPTIMIZE setting (default is false)
if set to true, you will have to stage.update() after setting certain properties
and stage.update() in change event to see component change its graphics

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)

EVENTS
dispatches a "change" event when a pad changes (but not when setting selectedIndex property)

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+66
	zim.Pad = function(width, cols, rows, keys, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, spacing, currentEnabled, corner, labelColor, gradient, gloss, backing, rollBacking, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, style, group, inherit) {
		var sig = "width, cols, rows, keys, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, spacing, currentEnabled, corner, labelColor, gradient, gloss, backing, rollBacking, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, style, group, inherit";
		var duo; if (duo = zob(zim.Pad, arguments, sig, this)) return duo;
		z_d("66");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Pad";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		// the other parameters will be handled by the Tabs object for each row
		if (zot(width)) width = DS.width!=null?DS.width:150;
		if (zot(cols)) cols = DS.cols!=null?DS.cols:3;
		if (zot(rows)) rows = DS.rows!=null?DS.rows:cols;
		if (zot(keys)) {
			if (DS.keys!=null) {keys = DS.keys}
			else {keys = []; for (var i=1; i<=rows*cols; i++){keys.push(i);}}
		}
		if (zot(currentEnabled)) currentEnabled = DS.currentEnabled!=null?DS.currentEnabled:true;
		if (zot(spacing)) spacing = DS.spacing!=null?DS.spacing:1;

		var that = this;
		var myIndex;

		this.cols = cols; // read only
		this.rows = rows;

		var height = width / cols - spacing;
		var rowTabs = [];
		var count = 0;
		var r;
		this.labels = [];
		this.buttons = [];
		for (var i=0; i<rows; i++) {
			var rowKeys = [];
			for (var j=0; j<cols; j++) {
				rowKeys.push((keys[count]!=null) ? keys[count] : "");
				count++;
			}
			var corn = DS.corner!=null?DS.corner:corner;
			if (corn && !Array.isArray(corn)) corn = [corn,corn,corn,corn];
			r = rowTabs[i] = new zim.Tabs({
				width:width,
				height:height,
				tabs:rowKeys,
				backgroundColor:DS.backgroundColor!=null?DS.backgroundColor:backgroundColor,
				rollBackgroundColor:DS.rollBackgroundColor!=null?DS.rollBackgroundColor:rollBackgroundColor,
				selectedBackgroundColor:DS.selectedBackgroundColor!=null?DS.selectedBackgroundColor:selectedBackgroundColor,
				color:DS.color!=null?DS.color:color,
				rollColor:DS.rollColor!=null?DS.rollColor:rollColor,
				selectedColor:DS.selectedColor!=null?DS.selectedColor:selectedColor,
				spacing:DS.spacing!=null?DS.spacing:spacing,
				currentEnabled:DS.currentEnabled!=null?DS.currentEnabled:currentEnabled,
				corner:corn,
				backing:DS.backing!=null?DS.backing.clone():backing,
				rollBacking:DS.rollBacking!=null?DS.rollBacking.clone():rollBacking,
				base:null,
				keyEnabled:false,
				gradient:DS.gradient!=null?DS.gradient:gradient,
				gloss:DS.gloss!=null?DS.gloss:gloss,
				wait:DS.wait!=null?DS.wait:wait,
				waitTime:DS.waitTime!=null?DS.waitTime:waitTime,
				waitBackgroundColor:DS.waitBackgroundColor!=null?DS.waitBackgroundColor:waitBackgroundColor,
				rollWaitBackgroundColor:DS.rollWaitBackgroundColor!=null?DS.rollWaitBackgroundColor:rollWaitBackgroundColor,
				waitColor:DS.waitColor!=null?DS.waitColor:waitColor,
				rollWaitColor:DS.rollWaitColor!=null?DS.rollWaitColor:rollWaitColor,
				waitModal:DS.waitModal!=null?DS.waitModal:waitModal,
				waitEnabled:DS.waitEnabled!=null?DS.waitEnabled:waitEnabled,
				group:group,
				style:false
			});
			this.labels = this.labels.concat(r.labels);
			this.buttons = this.buttons.concat(r.buttons);
			this.addChild(r);
			r.selectedIndex = -1;
			r.y = (height+spacing)*i;
			r.znum = i;
			r.on("change", pressKey);
		}
		this.tabs = rowTabs;
		function pressKey(e) {
			var r = e.target;
			that.selected = r.selected;
			that.text = r.text;
			that.label = r.label;
			var s = r.selectedIndex; // store selected then clear all in pad
			for (var i=0; i<rowTabs.length; i++) {
				rowTabs[i].selectedIndex = -1;
			}
			r.selectedIndex = s; // restore selected
			myIndex = r.znum * cols + s; // calculate pad selected
			that.dispatchEvent("change");
			if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
		}

		Object.defineProperty(this, 'selectedIndex', {
			get: function() {
				return myIndex;
			},
			set: function(value) {
				myIndex = value;
				for (var i=0; i<rowTabs.length; i++) {
					rowTabs[i].selectedIndex = -1;
				}
				var tabNum = Math.floor(myIndex / cols);
				if (tabNum >= 0 && tabNum < that.tabs.length) {
					that.tabs[tabNum].selectedIndex = myIndex % cols;
				}
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				zenable(that, value);
			}
		});

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.Pad(width, cols, rows, keys, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, spacing, currentEnabled, corner, labelColor, gradient, gloss, backing, rollBacking, wait, waitTime, waitBackgroundColor, rollWaitBackgroundColor, waitColor, rollWaitColor, waitModal, waitEnabled, style, this.group, inherit));
		}
	}
	zim.extend(zim.Pad, zim.Container, "clone", "zimContainer", false);
	//-66

/*--
zim.ColorPicker = function(width, colors, cols, spacing, greyPicker, alphaPicker, startBackgroundColor, draggable, shadowColor, shadowBlur, buttonBar, circles, indicator, backgroundColor, keyArrows, container, style, group, inherit)

ColorPicker
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A traditional color picker which shows 256 Web colors by default or custom colors.
Can additionally show 16 greys and / or an alpha slider.
Picking on a color sets the swatch color and the selectedColor property.
OK dispatches a "change" event if the color changed or a close event if not.
The X dispatches a "close" event.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var cp = new ColorPicker();
cp.center(stage);
cp.on("change", function() {
	zog(cp.selectedColor); // #ffcc99, etc. after pressing OK
	zog(cp.selectedAlpha); // 0-1
});
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 500) the width of the color picker
colors - (default 256 Web colors) an optional list of colors ["red", "#CCC", etc.]
cols - (default 10) how many columns to use if you pass in custom colors
spacing - (default 2) is the space between the color squares
greyPicker - (default true unless one row) shows an extra 16 greys (set to false to hide these)
	for the default colors it also includes 2 starting colors that record last picked colors
alphaPicker - (default true unless one row) shows an alpha slider (set to false to hide this)
	the swatch has a black, grey and white backing underneath to show multiple alpha effects
startBackgroundColor - (default the last color in color array) the starting color
draggable - (default true (false if no buttonBar)) whether you can drag the component - set to false to not drag
	a small grip under the color text shows if draggable
shadowColor - (default rgba(0,0,0,.3)) set to -1 for no drop shadow
shadowBlur - (default 14) the blur of the shadow if shadow is set
buttonBar - (default true unless one row) set to false to hide the button bar with OK and X (close)
circles - (default false) set to true to show colors in circles rather than squares
indicator - (default true) set to false to remove indicator from currentBackgroundColor
backgroundColor - (default black) the color of the background
keyArrows - (default true) set to false to disable keyboard arrows
container - (default frame.zimDefaultFrame) if using show(), hide(), toggle() can set which container to center on
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
show() - show the picker (returns the picker for chaining)
hide() - hides the picker
toggle(state - default null) - shows if hidden and hides if showing (returns the picker for chaining)
	or pass in true to show picker or false to hide picker
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
selectedColor - gets or sets the selected color swatch
currentValue - same as selectedColor but consistent with other components
currentValueEvent - gets or sets the current value and dispatches a "change" event if set and changed
selectedAlpha - gets or sets the selected alpha (set does not work if alphaPicker is false)
selectedIndex - get or sets the selected index of the colorPicker
colors - read only array of colors in picker - not including greys
greys - read only array of greys in picker if the grey picker is set
toggled - read-only Boolean property as to whether picker is showing
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
swatch - gets the Rectangle that is the color swatch
swatchBacking - gets the zim Shape that is under the swatch (seen if alpha set low)
swatchText - gets the Label that shows the color text
grip - gets the createjs.Shape for the grip if the panel is dragable
background - gets the Rectangle that is the background (cp.background.color = "white" - now a backgroundColor parameter)
okBut - references the OK Button
closeBut - references the X Button
indicator - gets the zim shape that is the indicator (if indicator is true)
NOTE: alphaPicker is true:
alpaBacking - gets reference to the Rectangle that makes the backing for the alpha slider
alphaBut - the Button on the alpha slider
alphaSlider - the Slider for the alpha
alphaText - the Label for the alpha
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
keyFocus - get or set the keyboard focus on the component - see also zim.KEYFOCUS
	will be set to true if this component is the first made or component is the last to be used
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)

EVENTS
dispatches a "set" event when a different color or alpha is selected and updated in the picker if the buttonBar is showing
dispatches a "change" event when the OK button is activated and the color or alpha is different than before
	or if buttonBar is false dispatches "change" when a new color or alpha is selected
dispatches a "close" event if the OK button is activated and the color has not changed or the X button is pressed

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+67
	zim.ColorPicker = function(width, colors, cols, spacing, greyPicker, alphaPicker, startBackgroundColor, draggable, shadowColor, shadowBlur, buttonBar, circles, indicator, backgroundColor, keyArrows, container, style, group, inherit) {
		var sig = "width, colors, cols, spacing, greyPicker, alphaPicker, startBackgroundColor, draggable, shadowColor, shadowBlur, buttonBar, circles, indicator, backgroundColor, keyArrows, container, style, group, inherit";
		var duo; if (duo = zob(zim.ColorPicker, arguments, sig, this)) return duo;
		z_d("67");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "ColorPicker";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(width)) width = DS.width!=null?DS.width:500;
		if (zot(colors)) colors = DS.colors!=null?DS.colors:null;
		if (zot(colors)) standard = true;
		if (zot(cols)) cols = DS.cols!=null?DS.cols:10;
		if (zot(spacing)) spacing = DS.spacing!=null?DS.spacing:2;
		var oneRow = !standard&&colors.length>0&&colors.length<=cols;
		if (zot(alphaPicker)) alphaPicker = DS.alphaPicker!=null?DS.alphaPicker:(oneRow?false:true);
		if (zot(greyPicker)) greyPicker = DS.greyPicker!=null?DS.greyPicker:(oneRow?false:true);
		if (zot(shadowColor)) shadowColor = DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (zot(shadowBlur)) shadowBlur = DS.shadowBlur!=null?DS.shadowBlur:14;
		if (zot(buttonBar)) buttonBar = DS.buttonBar!=null?DS.buttonBar:(oneRow?false:true);
		if (zot(draggable)) {
			if (buttonBar) {
				draggable = DS.draggable!=null?DS.draggable:true;
			} else {
				draggable = DS.draggable!=null?DS.draggable:false;
			}
		}
		if (zot(circles)) circles = DS.circles!=null?DS.circles:false;
		if (zot(indicator)) {
			indicator = DS.indicator!=null?DS.indicator:false;
			if (!buttonBar) indicator = DS.indicator!=null?DS.indicator:true;
		}
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"black";
		if (zot(startBackgroundColor)) startBackgroundColor = DS.startBackgroundColor!=null?DS.startBackgroundColor:null;
		if (zot(keyArrows)) keyArrows = DS.keyArrows!=null?DS.keyArrows:true;

		var that = this;
		var stage;

		if (zot(container)) {
			if (zimDefaultFrame) container = zimDefaultFrame.stage;
		 	else return;
		} else if (!container.getBounds) {
			return;
		} else if (zot(container.getStage)) {
			return;
		}
		that.container = container;

		var secondLastBackgroundColor = "#e472c4"; // only used on standard colors
		var thirdLastBackgroundColor = "#50c4b7";
		var lastAlpha = 1;
		var myAlpha = 1;

		var box = new createjs.Shape(); // shape that holds all colors and greys
		this.addChild(box);
		box.x += spacing;
		box.y += spacing;

		var standard = false;
		var colorsTemp; var w;
		var greys = [];
		if (zot(colors)) {
			standard = true;
			var num = 6; // six sets 0,3,6,9,C,F - for Web colors
			var tot = num*num*num;
			num = Math.ceil(Math.pow(tot,1/2));
			w = (width - spacing)/18-spacing;
			var f = Math.floor(Math.pow(num*num, 1/3));
			colorsTemp = [];
			for (var i=0; i<6; i++) {
				for (var j=0; j<6; j++) {
					for (var k=0; k<6; k++) {
						colorsTemp.push("#" + con(i*3) + con(j*3) + con(k*3));
					}
				}
			}
			colors = []; // flip every six by six sideways and put on two lines
			var c, r, nC, nR;
			for (i=0; i<colorsTemp.length; i++) {
				c = Math.floor(i/6);
				r = i%6;
				if (c >= 6*3) {f = 1;} else {f = 0;}
				nC = c-f*6*3;
				nR = r+f*6;
				colors[nR*18+nC] = colorsTemp[i];
			}
			cols = 18;
			greys = [thirdLastBackgroundColor, secondLastBackgroundColor];
		} else {
			w = (width - spacing) / cols - spacing;
		}
		var rows = Math.ceil(colors.length/cols);

		var startAlpha = 1;
		var myColor = String(colors[colors.length-1]);
		if (!zot(startBackgroundColor)) {
			var matches = startBackgroundColor.match(/rgba\((.*)\)/)
			if (matches) {
				var c = matches[1].split(",");
				startAlpha = c.pop();
				startBackgroundColor = "rgb("+c.join(",")+")";
			}
			myColor = String(startBackgroundColor);
		}

		var lastBackgroundColor;
		if (standard) lastBackgroundColor = thirdLastBackgroundColor;

		function con(n) {
			n = Math.floor(n).toString(16);
			return n + "" + n;
		}

		var g = box.graphics; var f=0; var color, r, c, rX , rY;
		var borderColor = DS.borderColor!=null?DS.borderColor:null;
		var borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (!zot(borderColor) && zot(borderWidth)) borderWidth = 1;
		if (borderWidth && zot(borderColor)) borderColor = "#333";
		if (borderWidth) g.s(borderColor).ss(borderWidth);

		for (i=0; i<colors.length; i++) {
			c = i%cols;
			r = Math.floor(i/cols);
			rX = c*(w+spacing);
			rY = r*(w+spacing);
			if (circles) {
				g.f(colors[i]).dc(rX+w/2,rY+w/2,w/2);
			} else {
				g.f(colors[i]).r(rX,rY,w,w);
			}
		}
		var lastHeight = rY + w + spacing;

		that.colors = colors;

		var greyHeight = lastHeight;
		if (greyPicker) {
			for (i=0; i<16; i++) {
				greys.push("#"+con(i)+con(i)+con(i));
			}
			for (i=0; i<greys.length; i++) {
				c = Math.floor(i/cols);
				r = i%cols;
				rX = r*(w+spacing);
				rY = c*(w+spacing)+lastHeight;
				if (circles) {
					g.f(greys[i]).dc(rX+w/2,rY+w/2,w/2);
				} else {
					g.f(greys[i]).r(rX,rY,w,w);
				}
			}
			lastHeight = rY + w + spacing;
			var greyCols = cols;
			var greyRows = Math.ceil(greys.length/cols);
		}

		that.greys = greys;

		if (indicator) {
			indicator = this.indicator = circles ? new zim.Circle(w/2*.5, null, null, null, null, null, null, false) : new zim.Rectangle(w*.5, w*.5, null, null, null, null, null, null, false);
			indicator.alpha = .5;
			indicator.centerReg();
			this.addChild(indicator);
			function positionIndicator(i) {
				if (zot(i) || i < 0) {
					indicator.visible = false;
					return;
				} else {
					indicator.visible = true;
				}
				if (myColor == "#000" || myColor == "#000000" || myColor == "black") {
					indicator.color = "#222";
					indicator.alpha = 1;
				} else {
					indicator.color = "black";
					indicator.alpha = .5;
				}
				indicator.x = box.x + i%cols*(w+spacing) + w/2;
				indicator.y = box.x + Math.floor(i/cols)*(w+spacing) + w/2;
			}
			positionIndicator(colors.indexOf(myColor));
		}

		var margin = 10;

		if (alphaPicker) {
			var alpha = new zim.Container({style:false});
			alpha.setBounds(0,0,600,70);
			this.addChild(alpha);
			alpha.x = 0;
			alpha.y = lastHeight;

			var alphaBacking = this.alphaBacking = new zim.Rectangle(600-margin*2, 50, "#222", null, null, 0, null, null, false);
			alpha.addChild(alphaBacking);
			zim.centerReg(alphaBacking, alpha);

			var sliderBut = this.alphaBut = new zim.Button({width:20,height:30,backgroundColor:"darkorange",rollBackgroundColor:"orange",label:"",corner:0,hitPadding:20,style:false});
			var slider = this.alphaSlider = new zim.Slider({min:0,max:1,step:.05,button:sliderBut,barLength:600*.55,barWidth:2,barColor:"#999",vertical:false,useTicks:false,inside:false,style:false});
			slider.currentValue = !zot(startAlpha)?startAlpha:1;
			alpha.addChild(slider);
			slider.x = 40;
			slider.y = alpha.height/2;

			var alphaText = this.alphaText = new zim.Label({
				text:"Alpha: " + zim.decimals(startAlpha), size:30, color:"orange",
				backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", backgroundColor:"ignore", rollColor:"orange",
				group:this.group
			});
			alpha.addChild(alphaText);
			alphaText.x = slider.x + slider.bar.width + 40;
			alphaText.y = alpha.height/2 - alphaText.height/2;

			alpha.scaleX = alpha.scaleY = width / 600;

			slider.on("change", function() {
				alphaText.text = "Alpha: " + decimals(slider.currentValue);
				if (swatch) {
					swatch.alpha = myAlpha = slider.currentValue;
				}
				if (buttonBar) {
					that.dispatchEvent("set");
				} else {
					that.dispatchEvent("change");
				}
				if (that.stage) that.stage.update();
			});
			lastHeight += alpha.height-margin;
		}

		if (buttonBar) {
			var nav = new zim.Container({style:false});
			nav.setBounds(0,0,600,100);
			this.addChild(nav);
			nav.x = 0;
			nav.y = lastHeight+margin;

			var swatchText = this.swatchText = new zim.Label({
				align:"center",
				text:myColor.toUpperCase(), labelWidth:120, labelHeight:50, size:30, color:"orange", rollColor:"orange",
				backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", backgroundColor:"ignore",
				group:this.group
			});
			nav.addChild(swatchText);
			zim.centerReg(swatchText);
			swatchText.x = 200/2-10;
			swatchText.y = 50-2;

			if (draggable) {
				var grip = this.grip = new zim.Shape({style:false});
				nav.addChild(grip);
				grip.graphics.f("rgba(256,256,256,.25)").r(0,0,5,20).r(10,0,5,20).r(20,0,5,20).r(30,0,5,20);
				grip.x = 70; grip.y = 65;
				swatchText.y = 50-10;
				grip.mouseEnabled = false;
			}
			var closeBut = this.closeBut = new zim.Button({width:90, height:90, label:"X", backgroundColor:"#222", rollBackgroundColor:"#444", corner:0, style:false});
			nav.addChild(closeBut);
			closeBut.x = 600 - closeBut.width - margin;
			closeBut.y = 0;
			closeBut.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", function(){that.dispatchEvent("close");});

			var button = this.okBut = new zim.Button({width:150, height:90, label:"OK", backgroundColor:"#222", rollBackgroundColor:"#444", corner:0, style:false});
			nav.addChild(button);
			button.x = closeBut.x - button.width - margin;
			button.y = 0;
			button.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", doChange);

			var swatchBacking = this.swatchBacking = new zim.Shape({style:false});
			nav.addChild(swatchBacking);
			var g = swatchBacking.graphics;
			g.f("black").r(0.5,0.5,50,89).f("#666").r(50,0.5,50,89).f("white").r(100,0.5,49.5,89);
			swatchBacking.x = button.x - 150 - margin;
			swatchBacking.y = 0;

			var swatch = this.swatch = new zim.Rectangle(150, 90, myColor, null, null, null, null, null, false);
			nav.addChild(swatch);
			swatch.x = swatchBacking.x;
			swatch.y = 0;
			swatch.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", doChange);
			swatch.cursor = "pointer";
			swatch.alpha = myAlpha = startAlpha;

			nav.scaleX = nav.scaleY = width / 600;
			lastHeight += nav.height;
		} else {
			box.cursor = "pointer";
		}

		if (!alphaPicker && !buttonBar) {
			lastHeight -= margin - spacing;
		}

		var height = lastHeight + margin;
		this.setBounds(0,0,width,height);

		var background = this.background = new zim.Rectangle(width,height,backgroundColor, null, null, null, null, null, false);
		this.addChildAt(background,0);
		if (shadowColor != -1 && shadowBlur > 0) background.shadow = new createjs.Shadow(shadowColor, 8, 8, shadowBlur);

		function doChange(){
			if (myColor != lastBackgroundColor || myAlpha != lastAlpha) {
				if (standard && greyPicker) {
					thirdLastBackgroundColor = secondLastBackgroundColor;
					secondLastBackgroundColor = lastBackgroundColor;
					var lastBackgroundColors = [thirdLastBackgroundColor, secondLastBackgroundColor]
					for (i=0; i<2; i++) {
						var g = box.graphics;
						c = Math.floor(i/cols);
						r = i%cols;
						rX = r*(w+spacing);
						rY = c*(w+spacing)+greyHeight;
						greys[i] = lastBackgroundColors[i];
						g.f(background.color).r(rX-1,rY-1,w+2,w+2).f(lastBackgroundColors[i]).r(rX,rY,w,w);
					}
					if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
				}
				lastBackgroundColor = myColor;
				lastAlpha = myAlpha;

				that.dispatchEvent("change");
			} else {
				that.dispatchEvent("close");
			}
		}

		if (draggable) {
			var diffX, diffY;
			background.on("mousedown", function(e) {
				stage = e.target.stage;
				diffX = e.stageX/stage.scaleX - that.x;
				diffY = e.stageY/stage.scaleY - that.y;
				background.cursor = "move";
			});
			background.on("pressmove", function(e) {
				that.x = e.stageX/stage.scaleX-diffX;
				that.y = e.stageY/stage.scaleY-diffY;
				if (that.stage) that.stage.update();
			});
			background.on("pressup", function(e) {
				background.cursor = "default";
				if (that.stage) that.stage.update();
			});
		}

		var gridW = cols*(w+spacing);
		var gridH = rows*(w+spacing);
		if (greyPicker) {
			var greyW = greyCols*(w+spacing);
			var greyH = greyRows*(w+spacing);
		}
		box.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", function(e) {
			var index = zim.hitTestGrid(box, gridW, gridH, cols, rows, e.stageX, e.stageY, 0, 0, spacing, spacing);
			if (!zot(index)) {
				myColor = colors[index];
				if (buttonBar) {
					swatch.color = myColor;
					swatchText.text = String(colors[index]).toUpperCase();
					if (myColor != lastBackgroundColor) that.dispatchEvent("set");
				} else {
					doChange();
				}
			}
			if (greyPicker) {
				// note greyW not gridW
				index = null;
				index = zim.hitTestGrid(box, greyW, greyH, greyCols, greyRows, e.stageX, e.stageY, 0, gridH, spacing, spacing);

				if (!zot(index)) {
					myColor = greys[index];
					if (buttonBar) {
						swatch.color = myColor;
						swatchText.text = greys[index].toUpperCase();
						if (myColor != lastBackgroundColor) that.dispatchEvent("set");
					} else {
						doChange();
					}
				}
			}
			if (indicator) positionIndicator(colors.indexOf(myColor));
			if (buttonBar) {
				if (that.stage) that.stage.update();
			} else if (indicator) {
				if (that.stage) that.stage.update();
				// if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && that.stage) that.stage.update();
			}
			setAccessibility();
		});

		Object.defineProperty(this, 'selectedColor', {
			get: function() {
				return myColor;
			},
			set: function(value) {
				lastBackgroundColor = myColor = value;
				if (buttonBar) {
					swatch.color = myColor;
					swatchText.text = myColor;
					if (that.stage) that.stage.update();
				}
				if (indicator) positionIndicator(colors.indexOf(myColor));
				setAccessibility();
			}
		});

		Object.defineProperty(this, 'currentValue', { // alternate to selectedColor
			get: function() {
				return myColor;
			},
			set: function(value) {
				that.selectedColor = value;
			}
		});

		Object.defineProperty(this, 'currentValueEvent', { // currentValue and also triggers change event
			get: function() {
				return myColor;
			},
			set: function(value) {
				if (value != that.selectedColor) {
					that.selectedColor = value;
					that.dispatchEvent("change");
				}
			}
		});

		Object.defineProperty(this, 'selectedIndex', {
			get: function() {
				return colors.indexOf(myColor);
			},
			set: function(value) {
				lastBackgroundColor = myColor = colors[value];
				if (buttonBar) {
					swatch.color = myColor;
					swatchText.text = myColor;
					if (that.stage) that.stage.update();
				}
				if (indicator) positionIndicator(colors.indexOf(myColor));
				setAccessibility();
			}
		});

		Object.defineProperty(this, 'selectedAlpha', {
			get: function() {
				if (alphaPicker) {
					return decimals(slider.currentValue);
				} else {
					return 1;
				}
			},
			set: function(value) {
				if (alphaPicker) {
					lastAlpha = slider.currentValue = value;
					if (swatch) swatch.alpha = lastAlpha;
					if (alphaText) alphaText.text = "Alpha: " + decimals(slider.currentValue);
					if (that.stage) that.stage.update();
				}
			}
		});


		Object.defineProperty(this, 'colors', {
			get: function() {
				if (greyPicker) return colors.concat(greys);
				else return colors;
			},
			set: function(value) {
				if (zon) zog("Display - ColorPicker() colors is read only - make a new ColorPicker to change")
			}
		});

		if (typeof KEYFOCUS !== typeof undefined) zim.KEYFOCUS = KEYFOCUS;
		Object.defineProperty(this, 'keyFocus', {
			get: function() {
				return zim.KEYFOCUS == that;
			},
			set: function(value) {
				if (zns) zim.KEYFOCUS = that;
				else KEYFOCUS = that;
			}
		});
		if (keyArrows && zim.KEYFOCUS) setFocus();
		this.on("mousedown", function() {if (keyArrows) setFocus()});
		function setFocus() {that.keyFocus = true; var d=document.activeElement; if (d) d.blur();}

		function setAccessibility() {
			if (that.zimAccessibility) that.zimAccessibility.changeTitle(that, null, true);
		}

		this.keyDownEvent = function(e) {
			if (!that.stage) return;
			if ((that.zimAccessibility && that.focus) || (!that.zimAccessibility && that.keyFocus)) {
				var currentTemp = that.selectedIndex;
				if (e.keyCode == 37 || e.keyCode == 40) {
					currentTemp--;
					changeMe();
				} else if (e.keyCode == 38 || e.keyCode == 39){
					currentTemp++;
					changeMe();
				}
				function changeMe() {
					if (currentTemp < 0) currentTemp = that.colors.length-1;
					if (currentTemp > that.colors.length-1) currentTemp = 0;
					that.selectedIndex = currentTemp;
					that.dispatchEvent("change");
					if (that.stage) that.stage.update();
				}
			}
		}
		window.addEventListener("keydown", this.keyDownEvent);

		this.hide = function() {
			that.removeFrom();
			that.toggled = false;
			if (that.zimAccessibility) {
				var a = that.zimAccessibility;
				a.resize(that);
				if (accessibilityClicker) accessibilityClicker.focus();
				else that.zimTabTag.nextSibling.focus();
				setTimeout(function() {a.talk("ColorPicker has been closed.");}, 50);
			}
			return that;
		}

		var accessibilityClicker;
		this.show = function() {
			that.center(that.container);
			if (that.zimAccessibility) {
				var a = that.zimAccessibility;
				setTimeout(function(){if (a.activatedObject) accessibilityClicker = a.activatedObject.zimTabTag;}, 50);
				a.resize(that);
				a.tabIndex = that.zimTabIndex;
			}
			that.toggled = true;
			return that;
		}
		this.toggle = function(state) {
			if (state===true) that.show();
			else if (state===false) that.hide();
			else if (that.container.contains(that)) that.hide();
			else that.show();
			return that;
		}

		function decimals(n) {
			return Math.round(n*Math.pow(10, 2))/Math.pow(10, 2);
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.ColorPicker(width, standard?null:colors, cols, spacing, greyPicker, alphaPicker, startBackgroundColor, draggable, shadowColor, shadowBlur, buttonBar, circles, indicator, backgroundColor, keyArrows, container, style, this.group, inherit));
		}

		this.dispose = function() {
			window.removeEventListener("keydown", that.keyDownEvent);
			if (slider) slider.dispose();
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.ColorPicker, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-67

/*--
zim.Keyboard = function(labels, backgroundColor, color, shiftBackgroundColor, shiftHoldBackgroundColor, placeBackgroundColor, placeColor, cursorColor, shadeAlpha, borderColor, borderWidth, margin, corner, draggable, placeClose, shadowColor, shadowBlur, container, data, place, special, rtl, style, group, inherit)

Keyboard
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
The Keyboard class makes a keyboard ideal for mobile or touch screens.
Often, it seems the mobile keyboard can cause problems with layout.
This in-canvas keyboard requires much less testing and concern.
The Keyboard can work with ZIM Labels to give input text without a TextArea.
Thanks Frank Los for the initial design and coding of the Keyboard.
See https://zimjs.com/keyboard

NOTE: press and hold down the vowels for multiple vowel options
NOTE: currently, multi-line Label input is not supported
NOTE: the width of the Label can be set by the Label's lineWidth paremeter
NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// create Labels to capture the text from the keyboard
var text1 = new Label({text:"", backgroundColor:white}).pos(100,100);
var text2 = new Label({text:"", backgroundColor:white}).pos(100,200);

// create a new Keyboard and pass in the labels as an array
// or if just one label, then pass in the label
var keyboard = new Keyboard([text1, text2]);

// if just the letter is needed use the keydown event
keyboard.on("keydown", function(e) {
	zog(e.letter);
});

// create events to capture a mousedown on the labels
var text1Event = text1.on("mousedown", activate);
var text2Event = text2.on("mousedown", activate);
function activate(e) {
	keyboard.show();
	// remove the events when keyboard is active
	text1.off("mousedown", text1Event);
	text2.off("mousedown", text2Event);
}
keyboard.show(); // optionally show the keyboard to start

// add back the events to show the keyboard
keyboard.on("close", function() {
	text1.on("mousedown", text1Event);
	text2.on("mousedown", text2Event);
});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
labels - (default null) a ZIM Label to show letters in or an array of labels
	Keyboard will add a cursor to the Labels
	and provide management across multiple labels
	currently, multiline labels are not supported
	setting the label lineWidth will set the max width of the label
backgroundColor - (default "#333") an css color for the background color of the keys
color - (default "white") the color of the text
shiftBackgroundColor - (default "orange") the color of the active shift key
shiftHoldBackgroundColor - (default "red") the color of the active shift hold key
placeBackgroundColor - (default "50c4b7") the color of the arrow backings when placing cursor in label
placeColor - (default "50c4b7") the color of the arrow text when placing cursor in label
cursorColor - (default "50c4b7") the cursor color
shadeAlpha - (default .2) special keys are shaded darker by this alpha
margin - (default 5) the margin around the keyboard from the container width
corner - (default 30) the round of the corner (set to 0 for no corner)
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
draggable - (default false) set to true to show the drag handle at top right
placeClose - (default true) shows an x key to close the cursor placement menu
shadowColor - (default "rgba(0,0,0,.2)") set to -1 for no shadow
shadowBlur - (default 14) how blurred the shadow is if the shadow is set
container - (default zimCurrentFrame stage) if placing Keyboard in different container or stage
data - (default see below) pass in data for the letters on the three sets of keyboards
	Below is the default data - change any of the keys to change keyboard
	There must be three boards (you can request more)
	There must be a button specified on the fourth row to toggle to the second screen and back
	There must be a button on the second and third screen at the start of the third row
	to toggle between the second and third screen
	The "shift" and "delete" keys are optional and can be moved or removed
	Where 10 keys can fit, there is a maximum of 10 keys but there can be less
	Use the data property to get this array if desired:
	var data = [
		[
			["q","w","e","r","t","y","u","i","o","p"],
			["a","s","d","f","g","h","j","k","l"],
			["shift","z","x","c","v","b","n","m","backspace"],
			["?123"] // rest of bottom line automatically added
		],[
			["1","2","3","4","5","6","7","8","9","0"],
			["!","@","#","$","/","^","&","*","(",")"],
			["1/2","-","'", "\"",":",";",",","?","backspace"],
			["ABC"] // rest of bottom line automatically added
		],[
			["+","x","%","=","<",">","{","}","[","]"],
			["€","£","¥", "$", "￦", "~", "`","¤","♡","☆"],
			["2/2","_","\\","|","《","》","¡","¿","backspace"],
			["ABC"] // rest of bottom line automatically added
		]
	];
place (default true) - set to false to not add place arrows when selecting Label
special (default null) - set to a string to add a special key to the left of the space bar
rtl (default false) - (Experimental) set to true to use right-to-left text
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
show(index) - shows the Keyboard - use this rather than addTo(), etc.
	index (default null) specify the index of the labels array to show cursor in
hide() - hides the keyboard
toggle(state - default null) - shows if hidden and hides if showing (returns the keyboard for chaining)
	or pass in true to show keyboard or false to hide keyboard
addLabels(labels) - add a ZIM Label or an array of Labels to the labels list for the Keyboard
removeLabels(labels) - remove a ZIM Label or an array of Labels
showPlace() - show the place menu for cursor
hidePlace() - hide the place menu for cursor
resize() - scales the keyboard to the stage with margin and places at bottom of screen
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a clone of the Keyboard
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
data - get the data array for the keyboard - see the data parameter for details and to set value for data
labels - get the labels array - use addLabels() and removeLabels() to set
selectedLabel - the label with the cursor or -1 if no cursor
selectedIndex - the index of the cursor in the selected label or -1 if no cursor
type - holds the class name as a String
toggled - read-only Boolean that is true if keyboard is visible and false if not
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
Dispatches a "keydown" event with an event object having a letter property
	keyboard.on("keydown", function(e) {zog(e.letter);}); // logs letter pressed or "del" for delete
Dispatches a "special" event if the special parameter is used and the special key is pressed
Dispatches a "close" event when close keyboard icon at bottom right is pressed

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+67.2
	zim.Keyboard = function(labels, backgroundColor, color, shiftBackgroundColor, shiftHoldBackgroundColor, placeBackgroundColor, placeColor, cursorColor, shadeAlpha, borderColor, borderWidth, margin, corner, draggable, placeClose, shadowColor, shadowBlur, container, data, place, special, rtl, style, group, inherit) {
		var sig = "labels, backgroundColor, color, shiftBackgroundColor, shiftHoldBackgroundColor, placeBackgroundColor, placeColor, cursorColor, shadeAlpha, borderColor, borderWidth, margin, corner, draggable, placeClose, shadowColor, shadowBlur, container, data, place, special, rtl, style, group, inherit";
		var duo; if (duo = zob(zim.Keyboard, arguments, sig, this)) return duo;
		z_d("67.2");
		this.zimContainer_constructor(1000,400,null,null,false);
		this.type = "Keyboard";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(labels)) labels = DS.labels!=null?DS.labels:[];
		if (!Array.isArray(labels)) labels = [labels];
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"#333";
		if (zot(color)) color =  DS.color!=null?DS.color:"white";
		if (zot(shiftBackgroundColor)) shiftBackgroundColor = DS.shiftBackgroundColor!=null?DS.shiftBackgroundColor:"orange";
		if (zot(shiftHoldBackgroundColor)) shiftHoldBackgroundColor = DS.shiftHoldBackgroundColor!=null?DS.shiftHoldBackgroundColor:"red";
		if (zot(placeBackgroundColor)) placeBackgroundColor = DS.placeBackgroundColor!=null?DS.placeBackgroundColor:"#50c4b7";
		if (zot(placeColor)) placeColor = DS.placeColor!=null?DS.placeColor:color;
		if (zot(cursorColor)) cursorColor = DS.cursorColor!=null?DS.cursorColor:"#50c4b7";
		if (zot(shadeAlpha)) shadeAlpha = DS.shadeAlpha!=null?DS.shadeAlpha:.2;
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:"rgba(0,0,0,.1)";
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(margin)) margin = DS.margin!=null?DS.margin:5;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:30;
		if (zot(draggable)) draggable = DS.draggable!=null?DS.draggable:false;
		if (zot(placeClose)) placeClose = DS.placeClose!=null?DS.placeClose:true;
		if (zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.2)";
		if (zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:14;
		if (zot(data)) data = DS.data!=null?DS.data:[
				[
					["q","w","e","r","t","y","u","i","o","p"],
					["a","s","d","f","g","h","j","k","l"],
					["shift","z","x","c","v","b","n","m","backspace"],
					["?123"] // rest of bottom line automatically added
				],[
					["1","2","3","4","5","6","7","8","9","0"],
					["!","@","#","$","/","^","&","*","(",")"],
					["1/2","-","'", "\"",":",";",",","?","backspace"],
					["ABC"] // rest of bottom line automatically added
				],[
					["+","x","%","=","<",">","{","}","[","]"],
					["€","£","¥", "$", "￦", "~", "`","¤","♡","☆"],
					["2/2","_","\\","|","《","》","¡","¿","backspace"],
					["ABC"] // rest of bottom line automatically added
				]
			];
		var that = this;
		that.data = data;
		if (zot(place)) place = DS.place!=null?DS.place:true;
		if (zot(rtl)) rtl = DS.rtl!=null?DS.rtl:false;

		var mess = "zim display - Keyboard(): Please pass in a reference to a container with bounds set";
		if (zot(container)) {
			if (zimDefaultFrame) {
				container = zimDefaultFrame.stage;
			} else {
				zog(mess);
				return;
			}
		} else if (!container.getBounds) {
			zog(mess);
			return;
		} else if (zot(container.stage)) {
			zog("zim display - Keyboard(): The container must have a stage property");
			return;
		}

		if (shadowColor != -1 && shadowBlur > 0) that.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
		var currentStage = zimDefaultFrame?zimDefaultFrame.stage:null;

		// ~~~~~~~~~~~~~~~~~  SETUP

		that.labels = labels;
		var maxWidth;
		var numH = 10;

		var botArray = ["@","",".","/","away"];
		if (!zot(special)) botArray.splice(1,0,special);

		var textKeys = zim.copy(data[0]);
		// textKeys[2].unshift("shift");
		// textKeys[2].push("backspace");
		textKeys[3] = textKeys[3].concat(botArray);

		var numberKeys1 = zim.copy(data[1]);
		// numberKeys1[2].push("backspace");
		numberKeys1[3] = numberKeys1[3].concat(botArray);

		var numberKeys2 = zim.copy(data[2]);
		// numberKeys2[2].push("backspace");
		numberKeys2[3] = numberKeys2[3].concat(botArray);

		// var textKeys = [
		// 	["q","w","e","r","t","y","u","i","o","p"],
		// 	["a","s","d","f","g","h","j","k","l"],
		// 	["shift","z","x","c","v","b","n","m","backspace"],
		// 	["?123","@","",".","/","away"]
		// ]
		// var numberKeys1 = [
		//     ["1","2","3","4","5","6","7","8","9","0"],
		//     ["!","@","#","$","/","^","&","*","(",")"],
		//     ["1/2","-","'", "\"",":",";",",","?","backspace"],
		//     ["ABC","@","",".","/","away"]
		// ];
		// var numberKeys2 = [
		//     ["+","x","%","=","<",">","{","}","[","]"],
		//     ["€","£","¥", "$", "￦", "~", "`","¤","♡","☆"],
		//     ["2/2","_","\\","|","《","》","¡","¿","backspace"],
		//     ["ABC", "@","",".","/","away"]
		// ];
		var eLetters = ["ė","ē","ę","ê","é","ë","è"];//ĒĘÊÉËÈ
		var uLetters = ["ū","û","ú","ü","ù"];//ŪÛÚÜÙ
		var iLetters = ["ī","į","ì","í","ï","î"];//ĪĮÌÍÏÎ
		var oLetters = ["ō","œ","ø","õ","ô","ó","ö","ò"];// ŌŒØÕÔÓÖÒ
		var aLetters = ["ā","ã","å","â","á","ä","à","æ"];// ĀÃÅÂÁÄÀÆ
		var nLetters = ["ñ","ń"];

		var textKeyButtons = [];

		//maateenheid horizontaal
		var size = (1000-(9*5))/numH;

		//statuses toestenbord
		var statuses = {
		    def:"default",
		    shift:"shift",
		    number1:"number1",
		    number2:"number2"
		};
		var currentStatus = statuses.def;
		var currentKeyboard;
		var alternativeMenu;
		var textBlinker;
		var bigShiftOn = false;
		var shiftKey;
		var currentLabel;
		var insertPoint = 0;
		var cursorShiftMenu;
		var buttonsCursor = [];
		var shiftKeyIcon;
		var backspaceIcon;
		var hideKeyBoardIcon;
		var dragButton;
		var dragY;

		makeIcons();
		makeButtons(currentStatus);
		if (draggable) makeDragButton();

		// ~~~~~~~~~~~~~~~~~  INTERACTIONS

		this.on("mousedown", buttonPressed);

		function buttonPressed(e) {
		    currentStage = that.stage;
		    if (alternativeMenu) {
		        that.removeChild(alternativeMenu);
		    }
		    if (cursorShiftMenu) {
		        if (buttonsCursor.indexOf (e.target) < 0) {
		            removeCursorShiftMenu();
		        }
		    }
		    if (!zot(e.target.name)) {
		        //WIJZIGINGEN STATUS keyboard
		        if (draggable && e.target === dragButton) {
					that.tickerMouseEvent = currentStage.on("stagemousemove", function(e) {
						that.mouseYAmount = e.stageY;
					});
		            zim.Ticker.add(startDragging);
		            that.on("pressup", stopDragging);
		            dragY = e.stageY-that.localToGlobal(0,0).y;
				} else if (!zot(special) && e.target.name === special) {
		            that.dispatchEvent("special");
				} else if (e.target.name === "shift") { //shift
		            shiftKeys();
		        } else if (e.target.toggle === "?123") { //nummers
		            that.removeChild(currentKeyboard);
		            makeButtons(statuses.number1);
		        } else if (e.target.toggle === "ABC") { //teksten
		            that.removeChild(currentKeyboard);
		            currentStatus = statuses.def;
		            makeButtons();
		            if (bigShiftOn) shiftKeys(true);
		        } else if (e.target.toggle==="1/2") {
		            that.removeChild(currentKeyboard);
		            makeButtons(statuses.number2);
		        } else if (e.target.toggle==="2/2") {
		            that.removeChild(currentKeyboard);
		            makeButtons(statuses.number1);
		        } else if (e.target.name==="away") {
		            hideKeyboard();
					that.dispatchEvent("close");
		        } else if (e.target.name === "e"||
		                e.target.name === "u"||
		                e.target.name === "i"||
		                e.target.name === "o"||
		                e.target.name === "a"||
		                e.target.name === "n") { //VARIATIES LETTERS euioan
		            makeAlternativeLetters(e.target.name);
		        } else if (e.target.name === "return") {
		            addToLabel("\n");
		        } else if (e.target.name === "backspace") {
		            backspaceRemovesLetter();
		        } else if (e.target.name === "") {
		            addToLabel(" ");
		        } else {
		            addToLabel(e.target.name);
		        }
		        currentStage.update();
		    }
		}

		var shiftEvent;
		function shiftKeys(immediate) {
		    var bigShift = false;
		    var i;
		    //vanuit default
		    if (currentStatus === statuses.def) {
		        shiftKey.color = immediate?shiftHoldBackgroundColor:shiftBackgroundColor;
		        //keyboard veranderen
		        for(i=0; i<textKeyButtons.length-6-(zot(special)?0:1);i++) {
		            var tkb = textKeyButtons[i];
		            if (tkb.label.text.length > 0) {
		                if (tkb.name.length===1) {
		                    tkb.label.text = tkb.label.text.toUpperCase();
		                    tkb.label.centerReg(tkb).mov(0,6);
		                } else {
		                    tkb.label.centerReg(tkb);
		                }
		            }
		        }
		        if (!immediate) {
		            //na halve seconde gaat groot shift aan
		            bigShift = true;
		            zim.timeout(500, putBigShiftOn);
		            shiftEvent = that.on("pressup", doNotPutBigShiftOn);
		        }
		        currentStatus = statuses.shift;
		    //vanuit shift
		    } else {
		        shiftKey.color = backgroundColor;
		        bigShiftOn = false;
		        //keyboard veranderen
		        for(i=0; i<textKeyButtons.length-6;i++) {
		            var tkb = textKeyButtons[i];
		            if (tkb.label.text.length>0) {
		                if (tkb.name.length===1) {
		                    tkb.label.text = tkb.label.text.toLowerCase();
		                    tkb.label.centerReg(tkb).mov(0,3);
		                } else {
		                    tkb.label.centerReg(tkb);
		                }
		            }
		        }
		        currentStatus = statuses.def;
		    }
			currentKeyboard.updateCache();
		    that.stage.update();
		    function putBigShiftOn() {
		        if (bigShift) {
		            bigShiftOn = true;
		            shiftKey.color = shiftHoldBackgroundColor;
					currentKeyboard.updateCache();
		            that.stage.update();
		        }
		    }
		    function doNotPutBigShiftOn(e) {
		        that.off("pressup", shiftEvent);
		        bigShift = false;
		    }
		}

		// ~~~~~~~~~~~~~~~~~  ASSETS

		function makeButtons(which) {
		    var typeKeyboard;
		    var label;
		    var button;
		    var bakking;
		    var xPos = 0;
		    var yPos = 0;
		    var thisWidth;
		    var bigKey;
		    var thisIsSpacekey= false;
		    var thisKeyLetter;
		    var passesLetter;
		    var dark= false;
		    //zonder parameters maak ik letters
		    if (zot(which)) which = statuses.def;
		    //letters
		   	if (which === statuses.def) {
		        typeKeyboard = textKeys;
		    //nummers1
		    } else if (which === statuses.number1) {
		        typeKeyboard = numberKeys1;
		    //nummers 2
		    } else if (which === statuses.number2) {
		        typeKeyboard = numberKeys2;
		    }
		    //container maken
		    currentKeyboard = new zim.Container(1000,430,null,null,false).addTo(that);
		    //alle toetsen, door arrays heen wandelen
		    for (var i = 0; i<typeKeyboard.length; i++) {
				if (i<=1 || (which==statuses.def && i==2 && typeKeyboard[2][0] != "shift")) {
					xPos=(size/2+2.5)*(10-typeKeyboard[i].length);
				}
		        for (var j=0; j<typeKeyboard[i].length; j++) {
		            thisIsSpacekey = false;
		            thisKeyLetter = null;
		            dark= false;
		            if (typeKeyboard[i][j]=="backspace") {
		                bigKey = true;
		                thisKeyLetter = backspaceIcon;
		                dark= true;
		            } else if (typeKeyboard[i][j]=="shift") {
		                bigKey = true;
		                thisKeyLetter = shiftKeyIcon;
		            } else if ((i==3 || (which!=statuses.def && i==2)) && j==0) {
		                bigKey = true;
		                dark= true;
		            } else if (typeKeyboard[i][j]=="") {
		                bigKey = false;
		                thisIsSpacekey = true;
		            } else if (typeKeyboard[i][j]=="away") {
		                thisKeyLetter = hideKeyBoardIcon;
		                bigKey = true;
		                dark= true;
		            } else {
		                bigKey = false;
		            }
		            //brede toets: breedte instellen
		            if (bigKey) {
		                thisWidth = (size*1.5+2.5);
		            } else if (thisIsSpacekey) {
		                thisWidth = (size+5)*(zot(special)?4:3)-5;
		            } else {
		                thisWidth = size;
		            }
					button = new zim.Rectangle(thisWidth, size, backgroundColor, borderColor, borderWidth, corner, null, null, false).cur().addTo(currentKeyboard);
					if (dark) button.addChild(new zim.Rectangle(thisWidth, size, "black", null, null, corner, null, null, false).alp(shadeAlpha));

		            if (thisKeyLetter) {
		                button.label = label = new zim.Label({text:"", backgroundColor:"ignore", font:DS.font!=null?DS.font:null, style:false});
		            } else {
		                button.label = label = new zim.Label({
		                    lineWidth:10,
		                    lineHeight:25,
							font:DS.font!=null?DS.font:null,
		                    text:typeKeyboard[i][j],
		                    color:color,
		                    align:"center",
							style:false
		                });
		            }
		            //plaatje op bakking
		            if (thisKeyLetter) {
						var clone = thisKeyLetter.clone();
		                clone.scaleTo(button,70,70);
		                clone.centerReg(button);
		            }
		            if (!passesLetter) {
		                label.centerReg(button).mov(0,(!isNaN(label.text)) ? 7 : 3);
		                button.x = xPos;
		                button.y = yPos;
		                button.name = typeKeyboard[i][j];
						if (i==2 && j==0 && which == statuses.number1) button.toggle = "1/2";
						if (i==2 && j==0 && which == statuses.number2) button.toggle = "2/2";
						if (i==3 && j==0 && which == statuses.def) button.toggle = "?123";
						if (i==3 && j==0 && which != statuses.def) button.toggle = "ABC";
						if (button.toggle) label.mov(0,3);
		                textKeyButtons.push(button);
		                if (button.name == "shift") {
		                    shiftKey = button;
		                }
		                xPos = button.x + button.width+5;
		            } else {
		                passesLetter = false;
		                xPos += 67.33;
		            }
		        }
		        yPos += size+5;
		        xPos = 0;
		    }
			currentKeyboard.cache(borderWidth?-borderWidth:0, borderWidth?-borderWidth:0, borderWidth?currentKeyboard.width+borderWidth*2:currentKeyboard.width, borderWidth?currentKeyboard.height+borderWidth*2:currentKeyboard.height);
		}

		function makeAlternativeLetters(letter) {
		    var thisArray;
		    var mouseReleased = false;
		    var alternativeMenuIsmade = false;
		    var timeWait;
		    switch(letter) {
		        case "e":
		        thisArray = eLetters;
		        break;
		        case "u":
		        thisArray = uLetters;
		        break;
		        case "i":
		        thisArray = iLetters;
		        break;
		        case "o":
		        thisArray = oLetters;
		        break;
		        case "a":
		        thisArray = aLetters;
		        break;
		        case "n":
		        thisArray = nLetters;
		        break;
		        default:
		        break;
		    }
		    timeWait = zim.timeout(500, makeAlternatemenu);
		    var mouseUpEvent = that.on("pressup", mouseUp);
		    function mouseUp(e) {
		        mouseReleased = true;
		        that.off("pressup", mouseUpEvent);
		        if (!alternativeMenuIsmade) {
		            addToLabel(letter);
		        }
		    }
		    function makeAlternatemenu() {
		        var label,
		            bakking,
		            button,
					overlay,
		            xPos = 0,
		            thisLetter;
		        timeWait.clear();
		        if (!mouseReleased) {
		            alternativeMenuIsmade = true;
		            alternativeMenu = new zim.Container(1000,size,null,null,false).addTo(that, 0);
		            alternativeMenu.y = - size - 5;
		            for (var i=0; i<thisArray.length; i++) {
		                if (currentStatus === statuses.shift) {
		                    thisLetter = thisArray[i].toUpperCase();
		                } else {
		                    thisLetter = thisArray[i];
		                }
		                label = new zim.Label({
		                    lineWidth:10,
		                    lineHeight:25,
		                    text:thisLetter,
							font:DS.font!=null?DS.font:null,
		                    color:color,
		                    align:"center",
							style:false
		                });
		                button = new zim.Rectangle(size, size, backgroundColor, borderColor, borderWidth, corner, null, null, false).addTo(alternativeMenu);
		                overlay = new zim.Rectangle(size, size, "white", null, null, corner, null, null, false).alp(.2);
		                button.addChild(overlay);
		                label.center(button);
		                button.name = thisArray[i];
		                button.x = xPos;
		                xPos += size+5;
		            }
		            that.stage.update();
		        }
		    }
		}

		function makeIcons() {
		    //shift
		    shiftKeyIcon = new zim.Shape({style:false});
		    shiftKeyIcon.graphics.f (color).p("AhIFoIAAjYIixAAID5n3ID6H3IixAAIAADYgAjHBxICeAAIAADYIBTAAIAAjYICeAAIjImSg");
		    shiftKeyIcon.setBounds(-51/2,-72/2,51,72);
		    //backspace
		    backspaceIcon = new zim.Container({style:false});
		    var  backspaceShape1 = new zim.Shape({style:false});
		    backspaceShape1.graphics.f (color).p("ACgC+IigigIifCgQgGAGgJAAQgJAAgGgGQgGgGgBgJQABgJAGgGICgigIigifQgGgGgBgJQABgJAGgGQAGgGAJAAQAIAAAHAGICfCgICgigQAGgGAJAAQAJAAAGAGQAGAGABAJQgBAJgGAGIigCfICgCgQAGAGABAJQgBAJgGAGQgGAGgJAAQgJAAgGgGg");
		    backspaceShape1.setTransform(82.6,32);
		    backspaceShape1.addTo(backspaceIcon);
		    var  backspaceShape2 = new zim.Shape({style:false});
		    backspaceShape2.graphics.f (color).s().p("AkhFAQgcAAgUgUIkHj6QgVgUAAgeQAAgdAVgVIEHj6QAUgTAcAAINKAAQAdAAAUAUQAUAUAAAdIAAH1QAAAdgUATQgUAVgdAAgAk0kOIkGD8QgIAHAAALQAAALAIAIIEGD7QAIAHALAAINKAAQALAAAIgIQAHgHAAgLIAAn1QAAgLgHgIQgIgIgLAAItKAAQgLAAgIAHg");
		    backspaceShape2.setTransform(62.2,32);
		    backspaceShape2.addTo(backspaceIcon);
		    backspaceIcon.setBounds(0,0,125,64);
		    //keyboardAway
		    hideKeyBoardIcon = new zim.Container({style:false});
		    hideKeyBoardIcon.setBounds(0,0,147,86);
		    var hideKeyBoardIconArray = [
		        {p:("Ai+heIF9AAIi/C9g"),transform:[73.4,76]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[128.4,43.2]},
		        {p:("AnNAzIAAhlIObAAIAABlg"),transform:[73,43.2]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[18.8,43.2]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[128.2,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[114.5,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[100.8,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[87.1,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[73.4,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[59.7,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[46,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[32.3,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[18.6,29.5]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[128,15.8]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[114.3,15.8]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[100.6,15.8]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[86.9,15.8]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[73.2,15.8]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[59.5,15.8]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[45.8,15.8]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[32.1,15.8]},
		        {p:("AgyAzIAAhlIBlAAIAABlg"),transform:[18.4,15.8]},
		        {p:("AphEnQgzAAgkgkQglglAAgzIAAlVQAAgzAlglQAkgkAzAAITDAAQAzAAAkAkQAlAlAAAzIAAFVQAAAzglAlQgkAkgzAAgAqjjtQgcAcAAAnIAAFVQAAAnAcAbQAbAcAnAAITDAAQAnAAAcgcQAbgbAAgnIAAlVQAAgngbgcQgcgcgnAAIzDAAQgnAAgbAcg"),transform:[73.4,29.5]}
		    ];
		    var thisShape;
		    for (var i=0; i<hideKeyBoardIconArray.length;i++) {
		        thisShape = new zim.Shape({style:false});
		        thisShape.graphics.f (color).s().p(hideKeyBoardIconArray[i].p);
		        thisShape.setTransform(hideKeyBoardIconArray[i].transform[0],hideKeyBoardIconArray[i].transform[1]);
		        thisShape.addTo(hideKeyBoardIcon);
		    }
		}

		function makeDragButton() {
		    dragButton = new zim.Rectangle((size*1.5+2.5)+2.5,size,backgroundColor, borderColor, borderWidth, corner, null, null, false).addTo(that, 0).cur();
		    var rect;
		    for (var i=0; i<4;i++) {
		        rect = new zim.Rectangle(dragButton.width*0.4, 4,color, null, null, null, null, null, false).centerReg(dragButton).alp(.2);
		        rect.y -= -22 +(i*15);
		    }
		    dragButton.x = (8.5*size)+(40);
		    dragButton.y = - size - 5;
		    dragButton.name = "drag";
		}

		function hideKeyboard() {
		    that.hide();
		}

		function startDragging() {
			if (that.mouseYAmount) that.y = that.parent.globalToLocal(0, that.mouseYAmount-dragY).y;
		}

		function stopDragging() {
			currentStage.off("pressmousemove", that.tickerMouseEvent);
		    zim.Ticker.remove(startDragging);
		}


		// ~~~~~~~~~~~~~~~~~  LABELS AND CURSOR

		function positionBlinker() {
			if (!currentLabel || !textBlinker) return;
			// Dan Zen added 9.5.0 using CreateJS Label to avoid padding and backing issues and addition of bounds x for align issues
			var positionXBlinker = currentLabel.label.x + currentLabel.label.getBounds().x;
			for (var i=0; i<insertPoint;i++) {
				positionXBlinker += currentLabel.widthArray[i];
			}
			// Dan Zen added 9.5.0 padding for three types of Label (backing, backgroundColor and neither)
			textBlinker.heightOnly = currentLabel.getBounds().height*(currentLabel.backing&&zot(currentLabel.padding)?.9:1)-((currentLabel.paddingVertical&&currentLabel.background)?currentLabel.paddingVertical*2:0);
			textBlinker.center(currentLabel);
			textBlinker.x = positionXBlinker;
		}

		function makeCursorShiftmenu() {
			if (cursorShiftMenu) return that;
			if (!currentLabel) return that;
		    var tekens = placeClose?["<",">","x"]:["<",">"];
		    var label;
		    var bakking;
		    var button;
		    var point;
		    buttonsCursor = [];
		    point = currentLabel.localToLocal(0, 0, that);
		    cursorShiftMenu = new zim.Container({style:false}).addTo(that).pos({x:point.x, y:point.y+currentLabel.height+15, reg:true}).cur();
		    for (var i=0; i<tekens.length;i++) {
				bakking = new zim.Rectangle(size, size, placeBackgroundColor, borderColor, borderWidth, corner, null, null, false);
				if (tekens[i] == "x") new zim.Rectangle(size, size, "black", null, null, corner, null, null, false).alp(shadeAlpha).addTo(bakking);
		        button = new zim.Label({
		            lineWidth:10,
		            lineHeight:58,
		            text:tekens[i],
					backing:bakking,
					font:DS.font!=null?DS.font:null,
		            color:placeColor,
		            align:"center",
					valign:"center",
					style:false
		        }).addTo(cursorShiftMenu).cache();
		        button.x = i*(size+5);
		        buttonsCursor.push(button);
		    }
			point = currentLabel.localToLocal(0, 0, that);
			cursorShiftMenu.x = point.x;
			cursorShiftMenu.y = point.y+currentLabel.height+15;
			cursorShiftMenu.on("click", verschuifCursor);
		    function verschuifCursor(e) {
		        if (buttonsCursor.indexOf(e.target)==0) {
		            if (insertPoint > 0) insertPoint--;
		        } else if (buttonsCursor.indexOf(e.target)==1) {
		            if (insertPoint < currentLabel.text.length) insertPoint++;
		        } else {
					removeCursorShiftMenu();
				}
		        positionBlinker();
		    }
		    that.stage.update();
		}
		function removeCursorShiftMenu() {
			if (!cursorShiftMenu) return that;
			cursorShiftMenu.removeAllEventListeners();
			that.removeChild(cursorShiftMenu);
			cursorShiftMenu = null;
		}

		function addToLabel(letter) {
			if (!currentLabel) return;
			var measureField;
			var widthMeasureField;
			// backspace
			if (letter === "del") {
				if (currentLabel) currentLabel.text = [currentLabel.text.slice(0,insertPoint-1),currentLabel.text.slice(insertPoint)].join('');
				insertPoint--;
				makeWidthsArray();
			} else {
				if (currentStatus === statuses.shift) {
					letter = letter.toUpperCase();
				}
				var textBeforeCheck = currentLabel.text;
				measureField = currentLabel.clone().removeFrom();
				measureField.text = letter;
				widthMeasureField = measureField.label.getMeasuredWidth();
				if (!currentLabel.widthArray) {
					currentLabel.widthArray = [currentLabel.breedte];
				} else {
					currentLabel.widthArray.splice(insertPoint,0,widthMeasureField);
				}
				//toevoegen in string
				if (insertPoint<currentLabel.text.length) {
					currentLabel.text = [currentLabel.text.slice(0,insertPoint),letter,currentLabel.text.slice(insertPoint)].join('');
				} else {
					// currentLabel.text+="\u202E" + letter;
					currentLabel.text+=letter;
				}
				// if (currentLabel && currentLabel.width<maxWidth) {
				if (currentLabel && currentLabel.label.getBounds().width<maxWidth) {
					insertPoint++;
					positionBlinker();
				} else {
					currentLabel.text = textBeforeCheck;
				}
			}
			if (currentStatus === statuses.shift&&!bigShiftOn) {
				that.removeChild(currentKeyboard);
				makeButtons();
				currentStatus = statuses.def;
			}
			positionBlinker();
			var keyEvent = new createjs.Event("keydown");
			keyEvent.letter = letter;
			that.dispatchEvent(keyEvent);
			that.stage.update();
		}

		function activateLabel(e) {
			if (!that.stage) return;
			var point;
			var pointLabel;
			var sumUp = 0;
			var found = false;
			currentLabel = e.target;
			if (!currentLabel.widthArrayCheck) makeWidthsArray();
			maxWidth = currentLabel.label.lineWidth?currentLabel.label.lineWidth:10000;
			// Dan Zen added 9.5.0 point relative to actual CreateJS Label - to avoid padding and backing issues
			point = currentLabel.globalToLocal(e.stageX/e.target.stage.scaleX, e.stageY/e.target.stage.scaleY);
			point.x -= currentLabel.label.x; // adjusted for retina - label is CreateJS with broken globalToLocal
			point.y -= currentLabel.label.y;
			// point opzoeken in array textfield door op te tellen
			// for (var i=currentLabel.widthArray.length-1; i>=0; i--) {
			// 	sumUp += currentLabel.widthArray[i];
			// 	if (point.x < sumUp-currentLabel.widthArray[i]/2) {
			// 		insertPoint = i;
			// 		found = true;
			// 		break;
			// 	}
			// }
			// var rightOfLabel = currentLabel.getBounds().width + currentLabel.getBounds().x;
			// zog(rightOfLabel);
			// zog(currentLabel.widthArray)
			// for (var i=0; i<currentLabel.widthArray.length; i++) {
			// 	sumUp += currentLabel.widthArray.reverse()[i];
			// 	if (point.x < rightOfLabel - sumUp-currentLabel.widthArray.reverse()[i]/2) {
			// 		insertPoint = i;
			// 		found = true;
			// 		break;
			// 	}
			// }
			for (var i=0; i<currentLabel.widthArray.length; i++) {
				sumUp += currentLabel.widthArray[i];
				// Dan Zen added 9.5.0 addition of bounds x for when align is center or right
				if (point.x < sumUp-currentLabel.widthArray[i]/2+currentLabel.label.getBounds().x) {
					insertPoint = i;
					found = true;
					break;
				}
			}
			if (!found) {
				insertPoint = currentLabel.text.length;
			}
			positionBlinker();
			if (place && !cursorShiftMenu && currentLabel.text.length > 0) {
				makeCursorShiftmenu();
			}
			if (cursorShiftMenu && currentLabel) {
				if (currentLabel.text.length > 0) {
					point = currentLabel.localToLocal(0, 0, that);
					cursorShiftMenu.x = point.x;
					cursorShiftMenu.y = point.y+currentLabel.height+15;
				} else {
					removeCursorShiftMenu();
				}
			}
		}
		function makeWidthsArray() {
			if (!currentLabel) return;
			var measureField;
			currentLabel.widthArray = [];
			for (var i=0; i<currentLabel.text.length; i++) {
				measureField = currentLabel.clone().removeFrom();
				measureField.text = currentLabel.text[i];
				currentLabel.widthArray.push(measureField.label.getMeasuredWidth());
			}
			currentLabel.widthArrayCheck = true;
			positionBlinker();
		}

		function backspaceRemovesLetter() {
		    var removalOkay = true;
		    var timeOut;
		    function haalWeg() {
		        if (!currentLabel || currentLabel.text.length<1 || that.currentIndex==0) {
		            removalOkay = false;
		        }
		        if (removalOkay) {
		            removeLetter();
		            timeOut = zim.timeout(200, haalWeg);
		        }
		        if (currentLabel && currentLabel.text.length<1) {
		            stopRemoval();
		        }
		    }
		    function removeLetter() {
		        if (currentLabel && currentLabel.text.length>0) {
					if (that.selectedIndex > 0) addToLabel("del");
		            that.on("pressup", stopRemoval);
		        } else {
		            addToLabel("del");
		        }
		    }
		    function stopRemoval() {
		        removalOkay = false;
		        timeOut.clear();
		        that.off("pressup", stopRemoval);
		    }
		    removeLetter();
		    timeOut = zim.timeout(300, haalWeg);
		    that.stage.update();
		}

		function setLabels() {
			for (var i=0; i<labels.length; i++) {
		        labels[i].clickEvent = labels[i].on("click", activateLabel);
		    }
		}
		setLabels();

		function unsetLabels() {
			if (labels.length > 1) {
				for (var i=0; i<labels.length; i++) {
					labels[i].off("click", labels[i].clickEvent);
				}
			}
		}

		function makeCursor() {
			if (textBlinker) return;
		    currentLabel = labels[0];
			maxWidth = (currentLabel && currentLabel.label.lineWidth)?currentLabel.label.lineWidth:10000;
		    if (currentLabel) {
		        textBlinker = new zim.Rectangle(3, currentLabel.height-((currentLabel.paddingVertical&&currentLabel.background)?currentLabel.paddingVertical*2:0), cursorColor, null, null, null, null, null, false).center(currentLabel);;
		        textBlinker.x = 0;
				textBlinker.visible = false;
		        textBlinker.animate({
		            obj:{alpha:0},
		            rewind:true,
		            loop:true,
		            loopWait:750,
		            time:250,
		            id:"knipperTekst"
		        });
		        for (var j=0; j<labels.length; j++) {
		            labels[j].widthArray = [0];
		        }
				makeWidthsArray();
			}
		}
		makeCursor();
		function removeCursor() {
			zim.stopAnimate("knipperTekst");
			if (currentLabel) currentLabel.removeChild(textBlinker);
			textBlinker = null;
			currentLabel = null;
		}

		// ~~~~~~~~~~~~~~~ GETTER SETTER PROPS

		Object.defineProperty(this, 'selectedLabel', {
			get: function() {
				return currentLabel;
			},
			set: function(label) {
				var obj = {target:label};
				activateLabel(obj);
				that.hidePlace();
			}
		});

		Object.defineProperty(this, 'selectedIndex', {
			get: function() {
				return insertPoint;
			},
			set: function(index) {
				insertPoint = index;
				positionBlinker();
			}
		});

		// ~~~~~~~~~~~~~~~ METHODS

		this.show = function(index) {
			that.addTo(container);
			// that.resize();
			if (!zot(index)) {
				var obj = {target:labels[index]};
				activateLabel(obj);
			}
			if (textBlinker) textBlinker.visible = true;
			that.toggled = true;
			return that;
		}

		this.hide = function() {
			that.removeFrom(container);
			if (textBlinker) textBlinker.visible = false;
			currentStage.update();
			that.toggled = false;
			return that;
		}

		this.toggle = function(state) {
			if (state===true) that.show();
			else if (state===false) that.hide();
			else if (that.parent) that.hide();
			else that.show();
			return that;
		}

		this.showPlace = function() {
			makeCursorShiftmenu()
			return that;
		}

		this.hidePlace = function() {
			removeCursorShiftMenu();
			return that;
		}

		this.addLabels = function(labs) {
			if (!Array.isArray(labs)) labs = [labs];
			for (var i=labs.length-1; i>=0; i--) {
				var ind = labels.indexOf(labs[i]);
				if (ind >= 0 || labs[i].type != "Label") labs.splice(i, 1);
				else labs[i].widthArray = [0];
			}
			unsetLabels();
			labels = labels.concat(labs);
			setLabels();
			makeCursor();
			if (textBlinker) textBlinker.visible = true;
			return that;
		}

		this.removeLabels = function(labs) {
			if (!Array.isArray(labs)) labs = [labs];
			unsetLabels();
			for (var i=0; i<labs.length; i++) {
				var ind = labels.indexOf(labs[i]);
				if (ind >= 0) labels.splice(ind, 1);
			}
			setLabels();
			if (labels.length == 0) {
				if (currentLabel) removeCursor();
			} else {
				if (currentLabel && labels.indexOf(currentLabel) == -1) {
					removeCursor();
					makeCursor();
				}
			}
			return that;
		}

		var background = new zim.Rectangle(this.width, this.height, zim.clear, null, null, null, null, null, false).addTo(this).expand().bot();
		background.on("mousedown", function(){});
		background.on("click", function(){});

		this.resize = function() {
			that.scaleTo(currentStage, 100-margin*2/currentStage.width*100, 50-margin*2/currentStage.height*100);
			that.y = currentStage.height - that.height - margin;
			that.x = currentStage.width/2 - that.width/2;
			if (currentLabel && cursorShiftMenu) {
				point = currentLabel.localToLocal(0, 0, that);
		        cursorShiftMenu.x = point.x;
				cursorShiftMenu.y = point.y+currentLabel.height+15;
			}
			if (that.stage) that.stage.update();
			return that;
		}
		this.resize();

		if (that.selectedLabel) that.selectedIndex = that.selectedLabel.text.length;

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			var kb = new zim.Keyboard(labels, backgroundColor, color, shiftBackgroundColor, shiftHoldBackgroundColor, placeBackgroundColor, cursorColor, shadeAlpha, margin, corner, draggable, placeClose, shadowColor, shadowBlur, container, data, place, special, rtl, style, this.group, inherit);
			return that.cloneProps(kb);
		}
	}
	zim.extend(zim.Keyboard, zim.Container, "clone", "zimContainer", false);
	//-67.2

/*--
zim.Organizer = function(width, list, useAdd, useRemove, usePosition, autoAdd, autoRemove, autoPosition, addForward, removeForward, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, spacing, corner, keyEnabled, gradient, gloss, backdropColor, style, group, inherit)

Organizer
zim class - extends a zim.Tabs which extends a zim.Container which extends a createjs.Container

DESCRIPTION
A Tabs bar of interface for organizing a ZIM List.
This includes add, up, down, toTop, toBottom and remove icon buttons.
The Organizer can sit above the list and allow the user to add, remove and reorder the list.
Adding an item will add an empty button - this would need to be filled with the user input, etc.
If the user input is not ready, the autoAdd parameter can be set to false.
The change event will report an orgType of "add" and the add() method can be used when the input is ready.
The same for positioning or removing if desired.

See: https://zimjs.com/explore/organizer.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
	var organizer = new Organizer(250)
		.change(function () {
			if (organizer.orgType=="add") organizer.orgItem.text = "new";
		});
	new List({
		width:250
		organizer:organizer // pass the organizer to the list
	});
		.center()
		.mov(0,40);
	stage.update();

	// OR

	var list = new List(250)
		.center()
		.mov(0,40);
	var organizer = new Organizer(250, list)
		.center()
		.mov(0,-80)
		.change(function () {
			if (organizer.orgType=="add") organizer.orgItem.text = "new";
		});
	stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 300) width of Tabs - this will determine the height as the Buttons are square.
	There is no vertical version of an Organizer.
list - (default null) an ZIM List object to control - or null to add later with the list property
useAdd - default(true) set to false to not include the add button
useRemove - default(true) set to false to not include the remove button
usePosition - default(true) set to false to not include the position buttons (up, down, toTop, toBottom)
autoAdd - default(useAdd) set to false to not automatically add an item
	the "change" event will still be dispatched and the orgType will be "add"
	the add() method can be used to add user input for instance
autoRemove - default(useRemove) set to false to not automatically add an item
	the "change" event will still be dispatched and the orgType will be "remove"
	the remove() method can be used to manually remove
autoPosition - default(usePosition) set to false to not automatically position an item
	the "change" event will still be dispatched and the orgType will be "up", "down", "top", "bottom"
	the up(), down(), toTop(), toBottom() methods can be used to manually position
addForward - (default true) set to false to add item behind the current item (in list index) when adding
removeForward - (default true) set to false to select the item before the current item (in list index) when deleting
backgroundColor - (default "#777") the background color of the buttons
rollBackgroundColor - (default "#555") the background color of the button as rolled over
selectedBackgroundColor - (default "#444") the background color of the button when selected
color - (default "white") the text color of a deselected button when not rolled over
selectedColor - (default color) the text color of the selected button
rollColor - (default color) the rollover color
spacing - (default 2) the distance between the buttons
corner - (default 0) the corner radius of the tabs
   can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
keyEnabled - (default true) so tab key cycles through tabs, shift tab backwards
gradient - (default null) 0 to 1 (try .3) adds a gradient to the buttons
gloss - (default null) 0 to 1 (try .1) adds a gloss to the buttons
backdropColor - (default "#333") the background color of the list window (any CSS color)
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
add(index, item, clone) - manually add item at index - both are optional - index defaults to current index
	clone defaults to false - set to true to add a clone of the item
up(index) - move item up one index number in list - index defaults to current index
down(index) - move item down one index number in list - index defaults to current index
toTop(index) - move item to top of list (index 0) - index defaults to current index
toBottom(index) - move item bottom of list (index length-1) - index defaults to current index
remove(index) - manually remove item at index - index defaults to current index
setButtons() - manually rotate buttons to match List direction - automatically done when added to list or list is initially added to organizer
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: All Tab methods

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
list - the list the organizer is operating on
lastIndex - read-only selected index before change event is dispatched
orgIndex - read-only current index of list - same as list.currentIndex
orgItem - read-only selected item of list - same as list.selected
orgType - read-only type of button pressed - "add", "remove", "up", "down", "top", "bottom"
removedItem - a reference to the item that has been removed when removed button is pressed or remove() is called
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: All Tab properties

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "change" event when the buttons are pressed (may be the same button again)
	use orgType for what type "add", "remove", "up", "down", "top", "bottom"
	use orgIndex or list.currentIndex for current list index
	use orgItem or list.selected for selected item
	use removedItem for a removed item
	use lastIndex for index before change

ALSO: All Tab events

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+67.3
	zim.Organizer = function(width, list, useAdd, useRemove, usePosition, autoAdd, autoRemove, autoPosition, addForward, removeForward, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, spacing, corner, keyEnabled, gradient, gloss, backdropColor, style, group, inherit) {
		var sig = "width, list, useAdd, useRemove, usePosition, autoAdd, autoRemove, autoPosition, addForward, removeForward, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, spacing, corner, keyEnabled, gradient, gloss, backdropColor, style, group, inherit";
		var duo; if (duo = zob(zim.Organizer, arguments, sig, this)) return duo;
		z_d("67.3");

		this.group = group;
		var DS = style===false?{}:zim.getStyle("Organizer", this.group, inherit);

		if (zot(width)) width = DS.width!=null?DS.width:(list&&list.vertical)?list.width:300;
		if (zot(useAdd)) useAdd = DS.useAdd!=null?DS.useAdd:true;
		if (zot(useRemove)) useRemove = DS.useRemove!=null?DS.useRemove:true;
		if (zot(usePosition)) usePosition = DS.usePosition!=null?DS.usePosition:true;
		if (zot(autoAdd)) autoAdd = DS.autoAdd!=null?DS.autoAdd:useAdd;
		if (zot(autoRemove)) autoRemove = DS.autoRemove!=null?DS.autoRemove:useRemove;
		if (zot(autoPosition)) autoPosition = DS.autoPosition!=null?DS.autoPosition:usePosition;
		if (zot(addForward)) addForward = DS.addForward!=null?DS.addForward:true;
		if (zot(removeForward)) removeForward = DS.removeForward!=null?DS.removeForward:true;
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"#777";
		if (zot(rollBackgroundColor)) rollBackgroundColor = DS.rollBackgroundColor!=null?DS.rollBackgroundColor:"#555";
		if (zot(selectedBackgroundColor)) selectedBackgroundColor = DS.selectedBackgroundColor!=null?DS.selectedBackgroundColor:"#333";
		if (zot(color)) color = DS.color!=null?DS.color:"white";
		if (zot(rollColor)) rollColor = DS.rollColor!=null?DS.rollColor:color;
		if (zot(selectedColor)) selectedColor = DS.selectedColor!=null?DS.selectedColor:color;
		if (zot(spacing)) spacing = DS.spacing!=null?DS.spacing:1;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:0;
		if (zot(gradient)) gradient = DS.gradient!=null?DS.gradient:.2;
		if (zot(gloss)) gloss = DS.gloss!=null?DS.gloss:null;
		if (zot(backdropColor)) backdropColor = DS.backdropColor!=null?DS.backdropColor:null;
		if (!Array.isArray(corner)) {
			corner = [corner, corner, corner, corner];
		}

		var buttonStyle = {
			width:50,
			height:50,
			label:"",
			shadowColor:-1,
			gradient:gradient,
			corner:corner,
			backgroundColor:backgroundColor,
			rollBackgroundColor:rollBackgroundColor
		}

		var buttons = this.buttons = [];
		var addIcon = new zim.Shape();
		addIcon.graphics.f(color).p("AAAA4Ih6B8Ig5g4IB8h8Ih7h6IA4g5IB6B8IB7h8IA5A5Ih8B6IB8B7Ig5A5g");
		if (useAdd) buttons.push(new zim.Button({icon:addIcon.sca(.55).rot(-45).reg(1), inherit:buttonStyle}));
		if (usePosition) {
			var arrowIcon = new zim.Shape();
			arrowIcon.graphics.f(color).p("AiJieIETCeIkTCfg");
			buttons.push(new zim.Button({icon:arrowIcon.sca(.6).rot(list&&!list.vertical?180:-90).reg(-2), inherit:buttonStyle}));
			buttons.push(new zim.Button({icon:arrowIcon.clone().sca(.6).rot(list&&!list.vertical?0:90).reg(-2), inherit:buttonStyle}));
			var endIcon = new zim.Shape();
			endIcon.graphics.f(color).p("AiFCLIAAkVIBXAAIAAEVgAgiAAICoiHIAAEQg");
			buttons.push(new zim.Button({icon:endIcon.sca(.62).rot(list&&!list.vertical?0:90).reg(0), inherit:buttonStyle}));
			buttons.push(new zim.Button({icon:endIcon.clone().sca(.62).rot(list&&!list.vertical?180:-90).reg(0), inherit:buttonStyle}));
		}
		if (useRemove) buttons.push(new zim.Button({icon:addIcon.clone().rot(0).sca(.55), inherit:buttonStyle}));
		if (buttons.length==0) buttons = [""]
		this.zimTabs_constructor(width, width/buttons.length, buttons, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, false, spacing, true, false, corner, null, true, gradient, gloss, null, null, null, null, null, null, null, null, null, null, backdropColor);

		var that = this;
		that.list = list;
		that.type = "Organizer";

		that.setButtons = function() {
			if (usePosition) {
				that.buttons[1].icon.rotation = that.list&&!that.list.vertical?180:-90;
				that.buttons[2].icon.rotation = that.list&&!that.list.vertical?0:90;
				that.buttons[3].icon.rotation = that.list&&!that.list.vertical?0:90;
				that.buttons[4].icon.rotation = that.list&&!that.list.vertical?180:-90;
			}
		}

		var listEvent = false;
		var lastList = list;

		that.change(function (e) {
			// handle dynamically adding event to list, delayed list or changed list
			if ((!listEvent && that.list) || (that.list && that.list != lastList)) {
				if (that.list != lastList) {if (lastList) lastList.noChange();}
				that.list.change(function (e) {
					that.orgItem = that.list.selected;
					that.orgIndex = that.list.selectedIndex;
				});
				listEvent = true;
				lastList = that.list;
			}
			var val = e.target.selectedIndex;
			if (!useAdd) val++;
			if (!usePosition && (!useAdd || val>0)) val = 5;
			var badIndex = zot(that.list.selectedIndex) || that.list.selectedIndex<0;
			if (badIndex) that.currentValue = null;
			that.lastIndex = that.list.selectedIndex;
			switch(val) {
				case 0: // add
					if (autoAdd) {
						var index = Math.max(0,that.list.selectedIndex);
						that.add(index);
					}
					that.orgType = "add";
					break;
				case 1:  // up
					if (badIndex) break;
					if (autoPosition) {
						var index = that.list.selectedIndex;
						that.up(index);
					}
					that.orgType = "up";
					break;
				case 2:  // down
					if (badIndex) break;
					if (autoPosition) {
						var index = that.list.selectedIndex;
						that.down(index);
					}
					that.orgType = "down";
					break;
				case 3: // top
					if (badIndex) break;
					if (autoPosition) {
						var index = that.list.selectedIndex;
						that.toTop(index);
					}
					that.orgType = "top";
					break;
				case 4: // bottom
					if (badIndex) break;
					if (autoPosition) {
						var index = that.list.selectedIndex;
						that.toBottom(index);
					}
					that.orgType = "bottom";
					break;
				case 5: // remove
					if (badIndex) break;
					if (autoRemove) {
						var index = that.list.selectedIndex;
						that.remove(index);
					}
					that.orgType = "remove";
					break;
			}
		});

		this.add = function(index, item, clone) {
			if (zot(index)) index = that.lastIndex;
			if (zot(item)) item = "";
			if (addForward) {
				var index = Math.min(that.list.length, Math.max(0,that.list.selectedIndex)+1);
			} else {
				var index = Math.max(0,that.list.selectedIndex);
			}
			that.list.addAt(item, index, clone);
			that.list.selectedIndexPlusPosition = index;
			that.orgItem = that.list.selected;
			that.orgIndex = that.list.selectedIndex;
		}

		this.up = function(index) {
			if (zot(index)) index = that.lastIndex;
			var button = that.list.items[index];
			if (zot(button)) return;
			that.list.removeAt(1, index);
			that.list.addAt(button, index-1);
			that.list.selectedIndexPlusPosition = Math.max(0,index-1);
			that.orgItem = that.list.selected;
			that.orgIndex = that.list.selectedIndex;
		}

		this.down = function(index) {
			if (zot(index)) index = that.lastIndex;
			var button = that.list.items[index];
			if (zot(button)) return;
			that.list.removeAt(1, index);
			that.list.addAt(button, index+1);
			that.list.selectedIndexPlusPosition = Math.min(that.list.length-1,index+1);
			that.orgItem = that.list.selected;
			that.orgIndex = that.list.selectedIndex;
		}

		this.toTop = function(index) {
			if (zot(index)) index = that.lastIndex;
			var button = that.list.items[index];
			if (zot(button)) return;
			that.list.removeAt(1, index);
			that.list.addAt(button, 0);
			that.list.first();
			that.orgItem = that.list.selected;
			that.orgIndex = that.list.selectedIndex;
		}

		this.toBottom = function(index) {
			if (zot(index)) index = that.lastIndex;
			var button = that.list.items[index];
			if (zot(button)) return;
			that.list.removeAt(1, index);
			that.list.addAt(button, that.list.length);
			that.list.last();
			that.orgItem = that.list.selected;
			that.orgIndex = that.list.selectedIndex;
		}

		this.remove = function(index) {
			if (zot(index)) index = that.lastIndex;
			that.removedItem = that.list.selected;
			var index = Math.max(0,that.list.selectedIndex);
			that.list.removeAt(1, index);
			if (removeForward) {
				var index = Math.max(0,that.list.selectedIndex);
			} else {
				var index = Math.max(0,that.list.selectedIndex-1);
			}
			that.list.selectedIndexPlusPosition = Math.min(that.list.length-1, index);
			that.orgItem = that.list.selected;
			that.orgIndex = that.list.selectedIndex;
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			return that.cloneProps(new zim.Organizer(width, list, useAdd, useRemove, usePosition, autoAdd, autoRemove, autoPosition, addForward, removeForward, backgroundColor, rollBackgroundColor, selectedBackgroundColor, selectedRollBackgroundColor, color, rollColor, selectedColor, selectedRollColor, spacing, corner, keyEnabled, gradient, gloss, backdropColor, style, that.group, inherit));
		}
	}
	zim.extend(zim.Organizer, zim.Tabs, "clone", "zimTabs", false);
	//-67.3

/*--
zim.Loader = function(width, height, label, backgroundColor, rollBackgroundColor, color, rollColor, borderColor, borderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss, dashed, backing, rollBacking, rollPersist, icon, rollIcon, toggle, toggleBacking, rollToggleBacking, toggleIcon, rollToggleIcon, toggleEvent, frame, style, group, inherit)

Loader
zim class - extends a zim.Button which extends a zim.Container

DESCRIPTION
Loader lets you upload images and access them as a Bitmap (available in the loaded event function)
Loader uses the HTML input type=file tag and overlays this with a createjs DOMElement.
Loader is a Button so can be displayed for the user to click on.
It defaults to a dashed line region as you can also drag and drop files to the loader.
You can also save an image using the save() method to a new browser window for the user to save

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var loader = new Loader({
	frame:frame,
	label:"UPLOAD PIC OR DROP PICS HERE",
	width:700,
	height:400,
	corner:50
}).center(stage);
loader.on("loaded", function(e) {
	loop(e.bitmaps, function(bitmap){
		bitmap.centerReg(stage).drag();
	});
	loader.removeFrom(stage);
	stage.update();
});

// and to later save for instance in a button event:
saveButton.on("click") {
	loader.save(stage); // or some other container... can specify crop bounds too
}
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 250) the width of the button
height - (default 70) the height of the button
label - (default "UPLOAD PIC") ZIM Label or plain text
backgroundColor - (default "rgba(0,0,0,.05)") background color of button (any CSS color)
rollBackgroundColor - (default "rgba(0,0,0,.1)") rollover color of button backbground
color - (default "rgba(0,0,0,.5)") text color of button (any CSS color)
rollColor - (default color) rollover text color of button
borderColor - (default rgba(0,0,0,.3)) the color of the border
borderWidth - (default 1) thickness of the border
corner - (default 0) the round of the corner (set to 0 for no corner)
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
shadowColor - (default "rgba(0,0,0,.3)") set to -1 for no shadow
shadowBlur - (default 14) how blurred the shadow is if the shadow is set
hitPadding - (default 0) adds extra hit area to the button (good for mobile)
gradient - (default 0) 0 to 1 (try .3) adds a gradient to the button
gloss - (default 0) 0 to 1 (try .1) adds a gloss to the button
backing - (default null) a Display object for the backing of the button (eg. Shape, Bitmap, Container, Sprite)
	see ZIM Pizzazz module for a fun set of Button Shapes like Boomerangs, Ovals, Lightning Bolts, etc.
	https://zimjs.com/bits/view/pizzazz.html
rollBacking - (default null) a Display object for the backing of the rolled-on button
rollPersist - (default false) set to true to keep rollover state when button is pressed even if rolling off
icon - (default false) set to display object to add icon at the center of the button and remove label
	https://zimjs.com/bits/view/icons.html
rollIcon - (default false) set to display object to show icon on rollover
toggle - (default null) set to string to toggle with label or display object to toggle with icon or if no icon, the backing
rollToggle - (default null) set to display object to toggle with rollIcon or rollBacking if no icon
	there is no rollToggle for a label - that is handled by rollColor on the label
toggleEvent - (default mousedown for mobile and click for not mobile) what event causes the toggle
dashed - (default true) set to false to turn off the dashed for the border
frame - (default the zimDefaultFrame) a reference to the Frame (to scale and position the HTML tag)
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
resize() - call the resize event if the scale or position of the Loader is changed
	this will sync the location of the HTML input tag
	resize() is only needed if the scale or x, y of the Loader (or its container) is changed
	it is not needed for general window resizing - the Loader handles this
save(content, x, y, width, height, url, cached, cachedBounds, type, data) - save a picture (supports ZIM DUO)
	content - the Display object to be saved such as a Container, Bitmap, etc.
	x, y, width, height - the cropping bounds on that object otherwise defaults to 0,0,stageW,stageH
	cached - (default false) set to true if the object is currently already cached
	cachedBounds - if you are saving a different bounds than was previously cached
		setting the bounds here (createjs.Rectangle) will restore the cache to the previous bounds
	type - (default "png") set to "jpeg" for jpeg
	data - (default false) set to true to save as base64 data - thanks Andi Ermi for the request
		otherwise save returns the object for chaining

Button methods:
setBacking(type, newBacking) - dynamically set any type of backing for button (if null removes backing for that type)
	Backing types are: "backing", "rollBacking", "toggleBacking", "rollToggleBacking", "waitBacking", "rollWaitBacking"
	note - all backing will have a pattern property if a pattern is set as a backing
setIcon(type, newIcon) - dynamically set any type of icon for button (if null removes icon for that type)
	Icon types are: "icon", "rollIcon", "toggleIcon", "rollToggleIcon", "waitIcon", "rollWaitIcon"
toggle(state) - forces a toggle of label if toggle param is string, else toggles icon if icon is set or otherwise toggles backing
	state defaults to null so just toggles
	pass in true to go to the toggled state and false to go to the original state
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
tag - the HTML input tag of type file - used for uploading
group - used when the object is made to add STYLE with the group selector (like a CSS class)

Button properties:
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
text - references the text property of the Label object of the button
label - gives access to the label
backgroundColor - get or set non-rolled on backing color (if no backing specified)
rollBackgroundColor - get or set rolled on backing color
color - get or set non-rolled on text color (if no icon specified)
rollColor - get or set rolled on text color
backing - references the backing of the button
rollBacking - references the rollBacking (if set)
icon - references the icon of the button (if set)
rollIcon - references the rollIcon (if set)
toggleObj - references the toggle object (string or display object if set)
rollToggle - references the rollToggle (if set)
toggled - true if button is in toggled state, false if button is in original state
enabled - default is true - set to false to disable
rollPersist - default is false - set to true to keep rollover state when button is pressed even if rolling off
focus - get or set the focus property of the Button used for tabOrder
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)

EVENTS
loaded - is dispatched when the image(s) are uploaded - the event object comes with the following properties:
	e.bitmaps - an array of Bitmap objects of the loaded images
	e.bitmap - the first Bitmap to be created from the loaded images
	e.lastBitmap - the last Bitmap to be created from the loaded images
	e.total - the total Bitmap objects to be created from the loaded images

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+67.5

	zim.Loader = function(width, height, label, backgroundColor, rollBackgroundColor, color, rollColor, borderColor, borderRollColor, borderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss, dashed, backing, rollBacking, rollPersist, icon, rollIcon, toggle, toggleBacking, rollToggleBacking, toggleIcon, rollToggleIcon, toggleEvent, frame, style, group, inherit) {
		var sig = "width, height, label, backgroundColor, rollBackgroundColor, color, rollColor, borderColor, borderRollColor, borderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss, dashed, backing, rollBacking, rollPersist, icon, rollIcon, toggle, toggleBacking, rollToggleBacking, toggleIcon, rollToggleIcon, toggleEvent, frame, style, group, inherit";
		var duo; if (duo = zob(zim.Loader, arguments, sig, this)) return duo;
		z_d("67.5");
		this.group = group;
		var DS = style===false?{}:zim.getStyle("Loader", this.group, inherit);

		if (zot(width)) width = DS.width!=null?DS.width:250;
		if (zot(height)) height = DS.height!=null?DS.height:70;
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"rgba(0,0,0,.05)";
		if (zot(rollBackgroundColor)) rollBackgroundColor = DS.rollBackgroundColor!=null?DS.rollBackgroundColor:"rgba(0,0,0,.1)";
		if (zot(color)) color = DS.color!=null?DS.color:"rgba(0,0,0,.5)";
		if (zot(rollColor)) rollColor = DS.rollColor!=null?DS.rollColor:color;
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:"rgba(0,0,0,.3)";
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:1;
		if (zot(dashed)) dashed = DS.dashed!=null?DS.dashed:true;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:0;
		if (zot(label)) label = DS.label!=null?DS.label:new zim.Label({
			text:"UPLOAD PIC", color:color, rollColor:rollColor, valign:"center", align:"center",
			backing:"ignore", shadowColor:"ignore", shadowBlur:"ignore", backgroundColor:"ignore",
			group:this.group
		});
		if (zot(frame)) {
			if (zimDefaultFrame) {
				frame = zimDefaultFrame;
			} else {
				if (zon) {zog("zim.Loader - please provide a reference to zim Frame");} return;
			}
		}
		// this.zimButton_constructor();

		this.zimButton_constructor(width, height, label, backgroundColor, rollBackgroundColor, color, rollColor, borderColor, borderRollColor, borderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss, dashed, backing, rollBacking, rollPersist, icon, rollIcon, toggle, toggleBacking, rollToggleBacking, toggleIcon, rollToggleIcon, toggleEvent, null, null, null, null, null, null, null, null, null, null, null, null, false);
		this.type = "Loader";
		var that = this;
		var stage = frame.stage;
		label = that.label;

		var uploadTag = that.tag = document.createElement("input");
		document.body.appendChild(uploadTag);
		uploadTag.setAttribute("type", "file");
		uploadTag.setAttribute("multiple", "multiple");
		uploadTag.setAttribute("aria-hidden", true);
		uploadTag.hidden = true;
		uploadTag.style.cssText = "border:thin solid grey; z-index:2; width:"+width+"px; height:" + height + "px; overflow:hidden; outline:none;"
			 + "position:absolute; left:0px; top:0px; display:none; cursor:pointer; opacity: 0; filter: alpha(opacity=0);"

		this.addEventListener('mousedown', function() { // added for zim.Accessibility
			uploadTag.click();
		});
		uploadTag.addEventListener('change', handleImage);
		var upload = new createjs.DOMElement(uploadTag);
		stage.addChild(upload);
		upload.alpha = 0;

		var pRatio = frame.retina?(window.devicePixelRatio || 1):1;
		this.resize = function() {
			if (!that.stage) {
				uploadTag.setAttribute("aria-hidden", true);
				uploadTag.hidden = true;
				uploadTag.style.display = "none";
				// uploadTag.previosSibling.hidden = true;
				// uploadTag.previosSibling.style.display = "none";
				return;
			}
			uploadTag.setAttribute("aria-hidden", false);
			uploadTag.hidden = false;
			uploadTag.style.display = "block";
			// uploadTag.previosSibling.hidden = false;
			// uploadTag.previosSibling.style.display = "block";
			setTimeout(function() {
				var point = that.localToGlobal(0, 0);
				if (frame.retina) {
					upload.x = frame.x/stage.scaleX + point.x/pRatio;
					upload.y = frame.y/stage.scaleY + point.y/pRatio;
					// CreateJS DOMElement is scaling tag as stage scales
					zim.sca(upload, that.scaleX/pRatio, that.scaleY/pRatio);
				} else {
					upload.x = frame.x + point.x * frame.scale;
					upload.y = frame.y + point.y * frame.scale;
					zim.sca(upload, frame.scale*that.scaleX, frame.scale*that.scaleY);
				}
				stage.update();
			}, 50);
			return that;
		}
		this.resize();
		that.on("added", function() {
			uploadTag.style.display = "block";
			uploadTag.hidden = false;
			uploadTag.style.display = "block";
			that.resize();
		});
		that.added(addedCallback);
		function addedCallback() {
			that.resize();
			setTimeout(function() {
				uploadTag.style.display = "block";
				uploadTag.hidden = false;
				uploadTag.style.display = "block";
			}, 50);
			that.on("added", addedCallback, null, true); // once
		}
		that.on("removed", function() {
			uploadTag.style.display = "none";
			uploadTag.hidden = true;
			uploadTag.style.display = "none";
		});
		frame.on("resize", that.resize);

		function handleImage(e) {
			var files;
			if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
				files = e.dataTransfer.files;
			} else {
				files = e.target.files;
			}
			var bitmaps = [];
			var firstBitmap;
			var lastBitmap;
			for (var i=0; i<files.length; i++) {
				(function(file) {
					var reader = new FileReader();
					reader.onload = function(event){
						var img = new Image();
						img.onload = function(){
							var bitmap = new zim.Bitmap(img);
							bitmaps.push(bitmap);
							if (bitmaps.length == 1) firstBitmap = bitmap;
							if (bitmaps.length == files.length) {
								var e = new createjs.Event("loaded");
								e.bitmaps = bitmaps;
								e.bitmap = firstBitmap;
								e.lastBitmap = bitmap;
								e.total = bitmaps.length;
								that.dispatchEvent(e);
								uploadTag.value = "";
							}
						}
						img.src = event.target.result;
					}
					reader.readAsDataURL(file);
				})(files[i]);
			};
		}
		var xhr = new XMLHttpRequest();
		if (xhr.upload) {
			uploadTag.addEventListener("drop", function(e) {
				// first imageLoader change event triggers so remove event then add it again later
				uploadTag.removeEventListener('change', handleImage);
				handleImage(e);
				setInterval(function() {uploadTag.addEventListener('change', handleImage);}, 100);
			});
		}

		this.save = function(content, filename, x, y, width, height, cached, cachedBounds, type, data) {

			var sig = "content, filename, x, y, width, height, cached, cachedBounds, type, data";
			var duo; if (duo = zob(that.save, arguments, sig)) return duo;
			if (zot(content)) content = frame.stage;

			if (zot(type)) type = "png";
			if (zot(filename)) {
				filename = "saved_" + String(zim.makeID("numbers", 5)) + "." + type;
			} else {
				var parts = filename.split(".");
				if (parts.length == 1) {
					filename += "." + type;
				} else {
					var types = ["png","jpg","jpeg"];
					var ind = types.indexOf(parts[parts.length-1].toLowerCase());
					if (ind == -1) {
						filename += "." + type;
					} else {
						type = types[ind];
					}
				}
			}
			if (zot(x)) x = 0;
			if (zot(y)) y = 0;
			if (zot(width)) width = (content.getBounds && content.getBounds()) ? content.getBounds().width : frame.width;
			if (zot(height)) height = (content.getBounds && content.getBounds()) ? content.getBounds().height : frame.height;


			content.cache(x, y, width, height);

			if (data) {
				var image = content.cacheCanvas.toDataURL('image/' + type);
				if (cached) {
					if (cachedBounds) content.cache(cashedBound.x, cashedBound.y, cashedBound.width, cashedBound.height);
				} else {
					content.uncache();
				}
				return image;
			}
			// if (!zot(url)) {
			// 	zim.async(url+"?data="+content.cacheCanvas.toDataURL('image/jpeg'), loaderReply);
			// 	function loaderReply(result) {
			// 		var e = new createjs.Event("saved");
			// 		e.result = result;
			// 		that.dispatchEvent(e);
			// 	}
			// // or to a script using zim.async (currently untested - will test and provide examples soon)
			// // saved - is dispatched when a file is saved to a script (needs the url parameter) - event object includes:
			// // e.result - the message sent back from the server in the zim.async.loaderReply('message')
			// } else {
				// disabled by Chrome https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/GbVcuwg_QjM%5B1-25%5D
				// zgo(content.cacheCanvas.toDataURL('image/'+type), "_blank");

				// iframe option
				// var win = window.open();
				// win.document.write("<html><head><title>saved</title></head><body style='margin:0px;padding:0px;overflow:hidden'><iframe src="+content.cacheCanvas.toDataURL('image/'+type)+" frameborder=0 style='overflow:hidden;height:100%;width:100%' height=100% width=100%></iframe></body></html>");
				// win.document.close();

				// var win = window.open();
				// win.document.write("<html><head><title>saved</title><s"+"cript>function test(){document.getElementById('pic').src='" +content.cacheCanvas.toDataURL('image/'+type)+"';}</s"+"cript></head><body style='margin:0px;padding:0px;overflow:hidden'><iframe id=pic frameborder=0 style='overflow:hidden;height:100%;width:100%' height=100% width=100%></iframe></body></html>");
				// win.document.close();
				// win.test();

				// Thanks Frank Loss for the prompting
				// Thanks Ken for the fiddle guidance - https://jsfiddle.net/AbdiasSoftware/7PRNN/
				var isMsEdge = /Edge\/(\d)+/.test(navigator.userAgent);
				if (isMsEdge){
					var win = window.open();
					win.document.write("<html><head><title>saved</title></head><body style='margin:0px;padding:0px;overflow:hidden'><img src="+content.cacheCanvas.toDataURL('image/'+type)+"></body></html>");
					win.document.close();
				} else {
					var a = document.createElement("a");
					a.setAttribute("download", filename);
					a.setAttribute("href", content.cacheCanvas.toDataURL('image/'+type));
					document.body.appendChild(a);
					a.click();
					document.body.removeChild(a);
				}

				// img option
				// var win = window.open();
				// win.document.write("<html><head><title>saved</title></head><body style='margin:0px;padding:0px;overflow:hidden'><img src="+content.cacheCanvas.toDataURL('image/'+type)+"></body></html>");
				// win.document.close();

			// }
			if (cached) {
				if (cachedBounds) content.cache(cashedBound.x, cashedBound.y, cashedBound.width, cashedBound.height);
			} else {
				content.uncache();
			}

			return that;
		}

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			var u = new zim.Loader(
				width, height, !zot(label)?label.clone():null, backgroundColor, rollBackgroundColor, color, rollColor, borderColor, borderWidth, corner, shadowColor, shadowBlur, hitPadding, gradient, gloss,
				!zot(backing)?backing.clone():null,
				!zot(rollBacking)?rollBacking.clone():null,
				rollPersist,
				!zot(icon)?icon.clone():null, !zot(rollIcon)?rollIcon.clone():null,
				toggle,
				!zot(toggleBacking)?toggleBacking.clone():null,
				!zot(rollToggleBacking)?rollToggleBacking.clone():null,
				!zot(toggleIcon)?toggleIcon.clone():null,
				!zot(rollToggleIcon)?rollToggleIcon.clone():null,
				toggleEvent, frame, style, this.group
			);
			return that.cloneProps(u);
		}
		this.dispose = function() {
			document.body.removeChild(uploadTag);
			this.zimButton_dispose();
			return true;
		}
	}
	zim.extend(zim.Loader, zim.Button, ["clone", "dispose"], "zimButton", false);
	//-67.5

/*--
zim.TextArea = function(width, height, size, padding, color, backgroundColor, borderColor, borderWidth, corner, shadowColor, shadowBlur, dashed, id, placeholder, readOnly, spellCheck, password, inputType, frame, expand, style, group, inherit)

TextArea
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
TextArea creates an input text field by overlaying an HTML TextArea.
The TextArea is then overlayed with the createjs DOMElement
and scaled and positioned with ZIM code. This can also be used if selectable text is required
Access to the HTML tag is provided with the TextArea tag property.
So CSS Styles can be applied to the HTML tag as with any HTML textarea tag
The TextArea comes with a ZIM Rectangle in behind that you can adjust with parameters
or remove completely if you so desire using the TextArea background property
ie. myTextArea.background.alpha=0; or myTextArea.removeChild(myTextArea.background)
Due to the HTML tag being overlayed, the TextArea.resize() must be called if it is moved
(This is called automatically when the stage is resized)

NOTE: rotation and skewing of Tag is not supported - although might work with custom CSS transformations

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var textArea = new TextArea(300, 200).center();

var label = new Label({text:""}).pos(20,20);
textArea.on("input", function() {
	label.text = textArea.text;
	stage.update();
});

// to set scrollBars on TextArea use CSS on the TextArea tag property:
textArea.tag.style.overflow = "auto"; // etc.

// if manually scaled or positioned (or container is scaled or positioned)
// then the TextArea must be resized with the resize method
textArea.sca(.5).mov(200);
textArea.resize();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 250) the width of the TextArea backing (the textarea field will be that less the padding*2)
height - (default 70) the height of the TextArea backing (the textarea field will be that less the padding*2)
	Note: to set scrollBars use CSS: textArea.tag.style.overflow = "auto";
size - (default 20) a Number for the font-size of the TextArea (do not use px on the end)
	to change the font, use CSS on the tag property: textArea.tag.style.fontFamily = "courier";
padding - (default 5) the pixels between the backing border and the HTML textarea
color - (default "#666") text color (any CSS color)
backgroundColor - (default "rgba(256,256,256,.1)") background color of box
borderColor - (default rgba(0,0,0,.1)) the color of the border
borderWidth - (default 1) thickness of the border
corner - (default 0) the round of the corner (set to 0 for no corner)
	can also be an array of [topLeft, topRight, bottomRight, bottomLeft]
shadowColor - (default null) the shadow color (css color) of a drop shadow
shadowBlur - (default null) pixels of how blurred the shadow is if the shadow is set - eg. 10
dashed - (default true) set to false to turn off the dashed for the border
id - (default null) a string id for the HTML textarea tag for CSS styling, etc.
placeholder - (default null) a string that is used for the HTML textarea tag placeholder parameter
readOnly - (default false) set to true to make TextArea read only (still selectable)
spellCheck - (default true) set to false to turn Browser spell check off
password - (default false) set to true to turn the field into a password field - single line only (uses input field type=password and not TextArea)
frame - (default the zimDefaultFrame) a reference to the Frame (to scale and position the HTML tag)
expand - (default 20) hit area around background to count as a press on TextArea - handy for dragging as HTML tag area will override mouse on canvas
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
setFocus(type) - sets the focus of the TextArea tag (thanks Armin for the prompting)
	type is a Boolean that defaults to true - set to false to make the TextArea blur (loose focus)
	might need timeout 100ms before setting
	see also focus property
resize() - call the resize event if the scale or position of the TextArea is changed
	this will sync the location of the HTML textarea tag
	resize() is only needed if the scale or x, y of the TextArea (or its container) is changed
	it is not needed for general window resizing - the TextArea handles this
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
currentValue - get or set the text content of the TextArea
text - the same as currentValue - for convenience...
focus - get or set if the TextArea tag has focus or use setFocus() to set (might need timeout 100ms before setting)
readOnly - set to true to not be able to edit or to false to be able to edit (always can select)
tag - the HTML textarea tag - just a regular HMTL form tag which can be styled
background - access to the Rectangle() used for the background
	you can remove this with yourTextArea.background.removeFrom(yourTextArea);
	or adjust it dynamically with any of the Rectangle properties like color
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation
keyFocus - get or set the keyboard focus on the component - see also zim.KEYFOCUS
	will be set to true if this component is the first made or component is the last to be used
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
focus, blur are dispatched when the text area gains and loses focus
input is dispatched when the text area is typed or pasted into
change is dispatched when the text area is different after losing focus
These are just the html events passed on through - note the difference between input and change!

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+67.6
	zim.TextArea = function(width, height, size, padding, color, backgroundColor, borderColor, borderWidth, corner, shadowColor, shadowBlur, dashed, id, placeholder, readOnly, spellCheck, password, inputType, frame, expand, style, group, inherit) {
		var sig = "width, height, size, padding, color, backgroundColor, borderColor, borderWidth, corner, shadowColor, shadowBlur, dashed, id, placeholder, readOnly, spellCheck, password, inputType, frame, expand, style, group, inherit";
		var duo; if (duo = zob(zim.TextArea, arguments, sig, this)) return duo;
		z_d("67.6");
		this.group = group;
		var DS = style===false?{}:zim.getStyle("TextArea", this.group, inherit);

		if (zot(width)) width = DS.width!=null?DS.width:250;
		if (zot(height)) height = DS.height!=null?DS.height:70;
		if (zot(size)) size = DS.size!=null?DS.size:20;
		if (zot(padding)) padding = DS.padding!=null?DS.padding:5;
		if (zot(color)) color = DS.color!=null?DS.color:"#666";
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"rgba(256,256,256,.1)";
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:"rgba(0,0,0,.1)";
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:null;
		if (borderColor < 0 || borderWidth < 0) borderColor = borderWidth = null;
		else if (borderColor!=null && borderWidth==null) borderWidth = 1;
		if (zot(corner)) corner = DS.corner!=null?DS.corner:0;
		if (!zot(shadowBlur) && zot(shadowColor)) shadowColor=DS.shadowColor!=null?DS.shadowColor:"rgba(0,0,0,.3)";
		if (!zot(shadowColor) && zot(shadowBlur)) shadowBlur=DS.shadowBlur!=null?DS.shadowBlur:10;
		if (zot(password)) password=DS.password!=null?DS.password:false;
		if (zot(inputType)) inputType=DS.inputType!=null?DS.inputType:false;
		if (zot(expand)) expand = DS.expand!=null?DS.expand:20;
		if (expand === true) expand = 20;
		if (zot(frame)) {
			if (zimDefaultFrame) {
				frame = zimDefaultFrame;
			} else {
				if (zon) {zog("zim.TextArea - please provide a reference to zim Frame");} return;
			}
		}

		this.zimContainer_constructor(width, height, null, null, false);
		this.type = "TextArea";
		var that = this;
		var stage = frame.stage;

		var background = this.background = new zim.Rectangle(width, height, backgroundColor, borderColor, borderWidth, corner, dashed, null, false).expand(expand);
		if (shadowColor != -1 && shadowBlur > 0) background.shadow = new createjs.Shadow(shadowColor, 3, 3, shadowBlur);
		that.addChild(background);
		var textareaTag = that.tag = inputType||password?document.createElement("input"):document.createElement("textarea");
		document.body.appendChild(textareaTag);
		if (!zot(id)) {
			textareaTag.setAttribute("id", id);
			textareaTag.setAttribute("name", id);
		}
		if (password) {
			textareaTag.setAttribute("type", "password");
		} else if (inputType) {
			textareaTag.setAttribute("type", inputType);
		}
		if (readOnly) textareaTag.readOnly = true;
		if (!spellCheck) textareaTag.spellcheck = false;
		if (!zot(placeholder)) textareaTag.setAttribute("placeholder", placeholder);
		textareaTag.style.cssText = "background-color:rgba(0,0,0,.01); color:"+color+"; "
			 + "resize:none; z-index:3; width:"+(width-padding*2)+"px; height:"+(height-padding*2)+"px; overflow:hidden; outline:none;"
			 + "font-size:"+size+"px; font-family:"+(DS.font!=null?DS.font:"arial")+"; border:none; position:absolute; left:0px; top:0px; display:none;"

		textareaTag.addEventListener('change', function() {that.dispatchEvent("change")});
		textareaTag.addEventListener('input', function() {that.dispatchEvent("input")});
		textareaTag.addEventListener('focus', function() {
			if (frame.zil) window.removeEventListener("keydown", frame.zil[0]);
			that.dispatchEvent("focus")
		});
		textareaTag.addEventListener('blur', function() {
			if (frame.zil) window.addEventListener("keydown", frame.zil[0]);
			that.dispatchEvent("blur")
		});
		var textarea = new createjs.DOMElement(textareaTag);
		textarea.alpha = 0;

		this.on('mousedown', function() {setTimeout(function(){textareaTag.focus();}, 100)}); // for zim Accessibility

		this.setFocus = function(type) {
			if (zot(type)) type = true;
			if (type) textareaTag.focus();
			else textareaTag.blur();
			return that;
		}

		var pRatio = frame.retina?(window.devicePixelRatio || 1):1;
		this.resize = function() {
			setTimeout(function() {
				var point = that.localToGlobal(padding, padding);
				if (frame.retina) {
					textarea.x = frame.x/stage.scaleX + point.x/pRatio;
					textarea.y = frame.y/stage.scaleY + point.y/pRatio;
					// CreateJS DOMElement is scaling tag as stage scales
					zim.sca(textarea, that.scaleX/pRatio, that.scaleY/pRatio);
				} else {
					textarea.x = frame.x + point.x * frame.scale;
					textarea.y = frame.y + point.y * frame.scale;
					zim.sca(textarea, frame.scale*that.scaleX, frame.scale*that.scaleY);
				}
				textarea.alpha = 1;
				if (that.stage) stage.update();
			}, 20);
			return that;
		}
		this.resize();
		that.added(addedCallback);
		function addedCallback() {
			stage.addChild(textarea);
			setTimeout(function() {textareaTag.style.display = "block";}, 50);
			that.resize();
			that.on("added", addedCallback, null, true); // once
		}
		that.on("removed", function() {
			stage.removeChild(textarea);
			textareaTag.style.display = "none";
		});
		frame.on("resize", that.resize);

		Object.defineProperty(this, 'currentValue', {
			get: function() {
				return textareaTag.value;
			},
			set: function(value) {
				textareaTag.value = value;
			}
		});

		Object.defineProperty(this, 'text', {
			get: function() {
				return textareaTag.value;
			},
			set: function(value) {
				textareaTag.value = value;
			}
		});

		Object.defineProperty(this, 'focus', {
			get: function() {
				return (document.activeElement == textareaTag);
			},
			set: function(value) {
				that.setFocus(value);
			}
		});


		Object.defineProperty(this, 'readOnly', {
			get: function() {
				return textareaTag.readOnly;
			},
			set: function(value) {
				textareaTag.readOnly = value;
			}
		});

		if (typeof KEYFOCUS !== typeof undefined) zim.KEYFOCUS = KEYFOCUS;
		Object.defineProperty(this, 'keyFocus', {
			get: function() {
				return zim.KEYFOCUS == that;
			},
			set: function(value) {
				zim.KEYFOCUS = that;
			}
		});
		if (!zim.KEYFOCUS) that.keyFocus = true;
		textareaTag.addEventListener("mousedown", function() {that.keyFocus = true;});

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			var u = new zim.TextArea(width, height, size, padding, color, backgroundColor, borderColor, borderWidth, corner, shadowColor, shadowBlur, dashed, id, placeholder, readOnly, spellCheck, password, inputType, frame, expand, style, this.group, inherit);
			return that.cloneProps(u);
		}
		this.dispose = function() {
			document.body.removeChild(textareaTag);
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.TextArea, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-67.6

//
/*--
zim.Tag = function(width, height, id, frame, backgroundColor, padding, paddingHorizontal, paddingVertical, expand, style, group, inherit)

Tag
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Creates a <div></div> with id of id and overlays it on the Canvas with the createjs DOMElement
The tag is scaled and positioned with ZIM code and can be filled with any HTML desired
Access to the HTML tag is provided with the tag property (so you can use innerHTML or style on this)
However a convenience innerHTML and style properties have been added to Tag
CSS Styles can be applied to the HTML tag as with any HTML div tag
Or use the chainable add() method to add a String of HTML (instead of setting innerHTML)
Due to the HTML tag being overlayed, the tag.resize() must be called if it is manually scaled or moved
(This is called automatically when the stage is resized)

SEE: https://zimjs.com/explore/tag.html

NOTE: rotation and skewing of Tag is not supported - although might work with custom CSS transformations

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var tag = new Tag(300, 60).center().add("<h1>TITLE TEXT</h1>");
tag.style.color = red;
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
width - (default 250) the width of the div tag
height - (default 70) the height of the div tag
	Note: to set scrollBars use CSS: tag.style.overflow = "auto";
id - (default zimTag_randomNumber) a string id for the HTML div tag.
frame - (default the zimDefaultFrame) a reference to the Frame (to scale and position the HTML tag)
backgroundColor - (default "rgba(0,0,0,0)") a ZIM Rectangle used as a background
padding - (default 10) inner padding between edge of background rectangle and HTML tag
paddingHorizontal - (default padding) inner horizontal padding between edge of background rectangle and HTML tag
paddingVertical - (default padding) inner vertical padding between edge of background rectangle and HTML tag
expand - (default 20) hit area around background to count as a press on Tag - handy for dragging as HTML tag area will override mouse on canvas
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
add(htmlString) - chainable method to add HTML to tag - calling add() again will append
	see innerHTML property as alternative or to overwrite the innerHTML
resize() - call the resize event if the scale or position of the tag is changed
	this will sync the location of the div tag
	resize() is only needed if the scale or x, y of the tag (or its container) is changed
	it is not needed for general window resizing - the Tag handles this
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes from parent, removes event listeners - must still set outside references to null for garbage collection

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
tagID - the assigned id of the tag
tag - the HTML div tag - just a regular HMTL div tag which can be styled
innerHTML - the innerHTML property of the tag (so myTag.tag.innerHTML is not needed)
background - access to the ZIM Rectangle used as the background
style - the style property of the tag (so myTag.tag.style is not needed)
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS: see the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+67.7
	zim.Tag = function(width, height, id, frame, backgroundColor, padding, paddingHorizontal, paddingVertical, expand, style, group, inherit) {
		var sig = "width, height, id, frame, backgroundColor, padding, paddingHorizontal, paddingVertical, expand, style, group, inherit";
		var duo; if (duo = zob(zim.Tag, arguments, sig, this)) return duo;
		z_d("67.7");
		this.group = group;
		var DS = style===false?{}:zim.getStyle("Tag", this.group, inherit);

		if (zot(width)) width = DS.width!=null?DS.width:250;
		if (zot(height)) height = DS.height!=null?DS.height:70;
		if (zot(id)) id = DS.id!=null?DS.id:"zimTag_" + zim.rand(10000);
		if (zot(frame)) {
			if (zimDefaultFrame) {
				frame = zimDefaultFrame;
			} else {
				if (zon) {zog("zim.TextArea - please provide a reference to zim Frame");} return;
			}
		}
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"rgba(0,0,0,0)";
		if (zot(padding)) padding = DS.padding!=null?DS.padding:10;
		if (zot(paddingHorizontal)) paddingHorizontal = DS.paddingHorizontal!=null?DS.paddingHorizontal:padding;
		if (zot(paddingVertical)) paddingVertical = DS.paddingVertical!=null?DS.paddingVertical:padding;
		if (zot(expand)) expand = DS.expand!=null?DS.expand:20;
		if (expand === true) expand = 20;

		this.zimContainer_constructor(width, height, null, null, false);
		this.type = "Tag";
		this.tagID = id;
		this.id = id;
		var that = this;
		var stage = frame.stage;

		var background = that.background = new zim.Rectangle(width+paddingHorizontal, height+paddingVertical, backgroundColor).center(this).expand(expand?expand:0);

		var tag = that.tag = document.createElement("div");
		document.body.appendChild(tag);
		tag.setAttribute("id", id);
		tag.setAttribute("name", id);

		tag.style.cssText = "resize:none; z-index:3; width:"+width+"px; height:"+height+"px; overflow:hidden; outline:none;"
			 + "position:absolute; left:0px; top:0px; display:none;"
		var cjsTag = new createjs.DOMElement(tag);
		cjsTag.alpha = 0;

		var pRatio = frame.retina?(window.devicePixelRatio || 1):1;
		this.resize = function() {
			setTimeout(function() {
				var point = that.localToGlobal(0, 0);
				if (frame.retina) {
					cjsTag.x = frame.x/stage.scaleX + point.x/pRatio;
					cjsTag.y = frame.y/stage.scaleY + point.y/pRatio;
					// CreateJS DOMElement is scaling tag as stage scales
					zim.sca(cjsTag, that.scaleX/pRatio, that.scaleY/pRatio);
				} else {
					cjsTag.x = frame.x + point.x * frame.scale;
					cjsTag.y = frame.y + point.y * frame.scale;
					zim.sca(cjsTag, frame.scale*that.scaleX, frame.scale*that.scaleY);
				}
				cjsTag.alpha = 1;
				if (that.stage) stage.update();
			}, 50);
			return that;
		}
		this.resize();
		that.added(addedCallback);
		function addedCallback() {
			stage.addChild(cjsTag);
			setTimeout(function() {tag.style.display = "block";}, 50);
			that.resize();
			that.on("added", addedCallback, null, true); // once
		}
		that.on("removed", function() {
			stage.removeChild(cjsTag);
			tag.style.display = "none";
		});
		frame.on("resize", that.resize);

		this.add = function(html) {
			tag.innerHTML += html;
			return that;
		}

		Object.defineProperty(this, 'innerHTML', {
			get: function() {
				return tag.innerHTML;
			},
			set: function(value) {
				tag.innerHTML = value;
			}
		});

		Object.defineProperty(this, 'style', {
			get: function() {
				return tag.style;
			},
			set: function(value) {
				tag.style = value;
			}
		});

		if (style!==false) zimStyleTransforms(this, DS);
		this.clone = function() {
			var u = new zim.Tag(width, height, id, frame, backgroundColor, padding, paddingHorizontal, paddingVertical, expand, style, this.group, inherit);
			return that.cloneProps(u);
		}
		this.dispose = function() {
			document.body.removeChild(tag);
			this.zimContainer_dispose();
			return true;
		}
	}
	zim.extend(zim.Tag, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-67.7



	// function to set enabled of components
	function zenable(t,v) {
		if (v) {
			t.mouseChildren = true;
			t.mouseEnabled = true;
			t._enabled = true;
		} else {
			t.mouseChildren = false;
			t.mouseEnabled = false;
			t._enabled = false;
		}
	}

////////////////  ZIM METHODS  //////////////

// Zim Methods adds functionality to CreateJS for multies (Interactive Features)
// functions in this module require createjs namespace to exist and in particular easel.js and tween.js
// available at http://createjs.com

// SUBSECTION ADDING AND REMOVING

/*--
obj.addTo = function(container, index, localToLocal)

addTo
zim DisplayObject method

DESCRIPTION
A wrapper function for addChild() / addChildAt() to add the obj to the container.
This allows us to chain more effectively:
var circle = new Circle().addTo(stage).drag();
Also, ZIM has obj.center(container) and obj.centerReg(container) functions
where the obj comes first followed by the container.
So it is a pain to flip things and use container.addChild(obj)
Now, we can use obj.addTo(container) and the object we are adding comes first.
The index parameter is similar to an addChildAt()
We can also use obj.removeFrom() to automatically remove from the container

EXAMPLE
var circle = new Circle(50, "red");
circle.addTo(stage); // or just circle.addTo(); // for the default frame's stage
// with chaining - and dragging:
var circle = new Circle(50, "red").addTo(stage).drag();

var rect = new Rectangle(100, 100, "blue");
rect.addTo(stage, 0); // place on bottom
END EXAMPLE

EXAMPLE
// Changing Coordinates
// An object may appear to change positions
// when you add from one container to another (the stage is a container too).
// This happens because the x and y origin inside the containers may be at different places
// yet the x and y property of the object remains unchanged.
// The localToLocal parameter converts between the coordinates and updates the x and y
// so that the object does not appear to jump - this defaults to true

var container = new Container().pos(100,100);
var rectangle = new Rectangle(200, 200, yellow).addTo(container);
// we want to drag the container and the circle
// but when the circle is in the container we want to drag the circle independently
// this leads to a tricky situation that can be solved as follows:
rectangle.on("mousedown", function() {container.drag({currentTarget:true});});
rectangle.on("pressup", function() {container.noDrag();});

var circle = new Circle(50, blue).center().drag();
// check to see if the circle is over the container and if so then add the circle
// try settin the third parameter to false to see what would normally happen
circle.on("pressup", function() {
	if (rectangle.hitTestReg(circle)) { // do not hitTest the container!
		circle.addTo(container); // keep position with default true as third parameter
	} else {
		circle.addTo(stage);
	}
	stage.update();
});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
container - (default first frame's stage) the container to add to
index - (default null) if provided will addChildAt the object at the index (0 being bottom)
localToLocal - (default true) makes object not move when added from one coordinate space to another
	this also may change the objects x and y properties - set to false not to do this
	localToLocal is ignored if object currently does not have a parent

RETURNS obj for chaining
--*///+47.5
	zim.addTo = function(obj, container, index, localToLocal) {
		var sig = "obj, container, index, localToLocal";
		var duo; if (duo = zob(zim.addTo, arguments, sig)) return duo;

		z_d("47.5");
		if (zot(obj)) {zog("zim methods - addTo(): please provide object"); return;}
		if (zot(container)) {
			if (zimDefaultFrame) {container = zimDefaultFrame.stage;}
			else {zog("zim methods - addTo(): please provide container"); return;}
		}
		if (zot(localToLocal)) localToLocal = true;
		if (!obj.parent) localToLocal = false;
		if (localToLocal) var point = obj.parent.localToLocal(obj.x, obj.y, container);
		if (zot(index) || isNaN(index)) {container.addChild(obj);}
		else {container.addChildAt(obj, index);}
		if (localToLocal) {obj.x = point.x; obj.y = point.y;}
		return obj;
	}//-47.5

/*--
obj.removeFrom = function(container)

removeFrom
zim DisplayObject method

DESCRIPTION
A chainable wrapper function for removeChild() that removes the obj from the container
Matches obj.addTo(container)
We have obj.removeFrom(container)

EXAMPLE
var circle = new Circle(50, "red");
circle.addTo(stage);
// later
circle.removeFrom(stage);
END EXAMPLE

PARAMETERS
container - (default the object's parent) the container to remove the object from

RETURNS obj for chaining
--*///+47.6
	zim.removeFrom = function(obj, container) {
		z_d("47.6");
		if (zot(obj)) {zog("zim methods - removeFrom(): please provide object"); return;}
		if (zot(container)) {
			if (obj.parent) obj.parent.removeChild(obj);
		}
		else container.removeChild(obj);
		return obj;
	}//-47.6

/*--
obj.added = function(call, interval, maxTime)

added
zim DisplayObject method

DESCRIPTION
Calls callback function when object is added to the stage
CreateJS has an "added" event that triggers when an object is added to another container
but this container may not be on the stage.
added polls several times quickly and then every 100ms to see if the object has a stage property
Once it does then it calls the callback and removes the interval

EXAMPLE
var circle = new Circle(50, "red");
circle.added(function() {zog("has stage");});
END EXAMPLE

PARAMETERS
callback - the function to call when added - will call right away if object is already added
	call will receive a reference to the stage and the object as parameters
interval - (default 100) time in ms to check - keeps repeating until stage is there or maxTime is reached
maxTime - (default null) time in ms to keep checking or forever if not provided

RETURNS id of interval so clearInterval(id) will stop added() from checking for stage
--*///+47.7
	zim.added = function(obj, call, interval, maxTime) {
		z_d("47.7");
		if (zot(obj) || zot(call) || typeof call != "function") return;
		if (zot(interval)) interval = 100;
		if (obj.stage) {(call)(obj.stage, obj); return;}
		var startTime = Date.now();
		var startCount = 0;
		var startID = setInterval(function() {
			if (startCount > 5) {
				clearInterval(startID);
				return;
			}
			startCount++;
			if (obj.stage) {
				(call)(obj.stage, obj);
				clearInterval(startID);
				clearInterval(id);
			}
		}, 10);
		var id = setInterval(function() {
			if (maxTime > 0 && startTime-Date.now()>maxTime) clearInterval(id);
			if (obj.stage) {
				(call)(obj.stage, obj);
				clearInterval(id);
			}
		}, interval);
		return id;
	}//-47.7

/*--
obj.centerReg = function(container, index, add)

centerReg
zim DisplayObject method

DESCRIPTION
Centers the registration point on the bounds - obj must have bounds set.
Will default to adding the object to the container.
If no container parameter is provided and the object is in a container,
centerReg will center the object in the current container. Thanks Bracer Jack for the suggestion.
If no container and the object is not in a container,
centerReg will center the object on the default frame's stage.
A convenience function for setting both registration points at once.
Also see center() for centering without changing the registration point.

EXAMPLE
var rect = new Rectangle(100, 100, "blue");
rect.centerReg(); // centers registration, centers and adds to stage
rect.animate({obj:{rotation:360}, time:1000, ease:"linear", loop:true});

// To just center the registration and not add it to a container
// use rect.centerReg(null, null, false);
// This is different than in the past where leaving out parameters would just center the registration
// The change is to match the default behaviour of addTo() and center() to add to stage without parameters
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
container - (default object's parent or if none, the first frame's stage) centers the object on and adds to the container
index - (default null) if provided will addChildAt the object at the index (0 being bottom)
	will be ignored if the add parameter is false
add - (default true) set to false to only center the object's registration
	setting to false will not remove the object if already added to a container

RETURNS obj for chaining
--*///+48
	zim.centerReg = function(obj, container, index, add) {
		var sig = "obj, container, index, add";
		var duo; if (duo = zob(zim.centerReg, arguments, sig)) return duo;
		z_d("48");
		if (zot(obj) || !obj.getBounds || !obj.getBounds()) {zog("zim methods - centerReg(): please provide object with bounds set"); return obj;}
		if (zot(add)) add = true;
		if (zot(container) && obj.parent && add) {
			container = obj.parent;
			if (zot(index)) index = obj.parent.getChildIndex(obj);
			obj.parent.removeChild(obj);
		}
		if (!zot(container) && container == obj.parent) {
			if (zot(index)) index = obj.parent.getChildIndex(obj);
			obj.parent.removeChild(obj);
		}
		var oB = obj.getBounds();
		obj.regX = oB.x + oB.width/2;
		obj.regY = oB.y + oB.height/2;
		return add ? zim.center(obj, container, index) : obj;
	}//-48

/*--
obj.center = function(container, index, add)

center
zim DisplayObject method

DESCRIPTION
Centers the object on the container.
Will default to adding the object to the container.
If no container parameter is provided and the object is in a container,
center will center the object in the current container. Thanks Bracer Jack for the suggestion.
If no container and the object is not in a container,
center will center the object on the default frame's stage.
Also see centerReg() for centering registration point at same time.

EXAMPLE
var rect = new Rectangle(100, 100, "blue");
rect.center(); // centers and adds to stage
// the below animation will be around the registration point at the top left corner
// this is usually not desired - see centerReg() when rotating and scaling
rect.animate({obj:{rotation:360}, time:1000, ease:"linear", loop:true});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
container - (default object's parent or if none, the first frame's stage) centers the object on and adds to the container
index - (default null) if provided will addChildAt the object at the index (0 being bottom)
	will be ignored if the add parameter is false
add - (default true) set to false to only center and not add the object to the container
	setting to false will not remove the object if already added to a container

RETURNS obj for chaining
--*///+48.1
	zim.centerCheck = false;
	zim.center = function(obj, container, index, add) {

		var sig = "obj, container, index, add";
		var duo; if (duo = zob(zim.center, arguments, sig)) return duo;
		if (!zim.centerCheck) {z_d("48.1"); zim.centerCheck = true;}
		if (zot(obj) || !obj.getBounds) {zog("zim.center(): please provide object with bounds"); return;}
		if (zot(container)) {
			if (obj.parent) container = obj.parent;
			else if (zimDefaultFrame) container = zimDefaultFrame.stage;
		}
		if (zot(container) || !container.getBounds)  {zog("zim.center(): please provide container with bounds"); return;}

		var oB = obj.getBounds();
		var cB = container.getBounds();

	 	if (zot(add)) add = true;
		if (add && container.addChild) {
            if (zot(index) || (typeof index === 'number' && isNaN(index))) {
                if (!container.contains(obj)) container.addChild(obj);
            } else {
                container.addChildAt(obj, index);
            }
        }
		if (zot(cB)) { // just center on container origin
			if (oB) {
				obj.x = oB.x + obj.regX - oB.width/2;
				obj.y = oB.y + obj.regY - oB.height/2;
			} else {
				obj.x = obj.y = 0;
			}

			return obj; // just add to container if no bounds on Container
		}
		if (zot(oB)) { // just set at middle of container
			obj.x = container.getBounds().width/2;
			obj.y = container.getBounds().height/2;
			return obj;
		}

		// get registration point of object in coordinates of the container
		var reg = obj.localToLocal(obj.regX, obj.regY, container);

		// get bounds of the object in global space even if object is rotated and scaled
		// this makes a rectangle surrounding a rotated object - so bigger but parallel edges to the x and y
		var glob = zim.boundsToGlobal(obj);

		// now project this rectangle into the container coordinates
		// passing in a rectangle (glob) will make this act on the rectangle rather than the bounds
		// flip (true) means we go from global to local in the container
		var loc = zim.boundsToGlobal(container, glob, true);

		// the positions are all in the container coordinate so do the calculation
		obj.x = cB.x + cB.width/2 - loc.width/2  + (reg.x-loc.x);
		obj.y = cB.y + cB.height/2 - loc.height/2  + (reg.y-loc.y);

		if (!add && container.getStage && container.stage && obj.parent) {
			var p = container.localToLocal(obj.x, obj.y, obj.parent);
			obj.x = p.x;
			obj.y = p.y;
		}
		return obj;
	}//-48.1

/*--
obj.place = function(id)

place
zim DisplayObject method

DESCRIPTION
Sets the object to drag and logs to the console the x and y.
This is for when building you can move an object around to position it
then when positioned, look at the console for the positioning code.
In your code, set the reported x and y and delete the place call.

EXAMPLE
circle.place("circle"); // lets you drag circle around - then see console
END EXAMPLE

PARAMETERS
id - (default null) the name of the object so that the log gives you complete code

RETURNS obj for chaining
--*///+49
	zim.place = function(obj, id) {
		z_d("49");
		if (zot(obj)) return;
		if (zot(id)) id = "obj";
		function report() {
			zog(id+".x = " + Math.round(obj.x) +  "; "+id+".y = " + Math.round(obj.y) + ";");
			zog(id+".loc(" + Math.round(obj.x) +  ", " + Math.round(obj.y) + ");");
		}
		zim.drag({obj:obj, currentTarget:true, dragCursor:"crosshair"});
		zog("place "+id+" - to get new position");
		obj.on("click", report);
		return obj;
	}//-49

/*--
obj.placeReg = function(id)

placeReg
zim DisplayObject method

DESCRIPTION
Gives draggable indicator to position a registration point in an object
This is for when building and when positioned, look at the console
for registration code and delete the placeReg call.

EXAMPLE
myContainer.placeReg("myContainer"); // lets you drag an indicator around - then see console
END EXAMPLE

PARAMETERS
id - (default null) the name of the object so that the log gives you complete code

RETURNS obj for chaining
--*///+49.5
	zim.placeReg = function(obj, id) {
		z_d("49.5");
		if (zot(obj)) return;
		var stage = obj.stage;
		if (zot(stage)) {zog("zim.placeReg() - add object to stage before calling placeReg()");	return obj;}
		if (zot(id)) id = "obj";
		function report() {
			var p = obj.globalToLocal(cursor.x, cursor.y);
			zog(id+".regX = " + Math.round(p.x) +  "; "+id+".regY = " + Math.round(p.y) + ";");
			zog(id+".reg(" + Math.round(p.x) +  ", " + Math.round(p.y) + ");");
		}
		var p = obj.parent.localToGlobal(obj.x, obj.y);
		var cursor = new zim.Shape(-25, -25, 50, 50,null,false).addTo(stage).pos({x:p.x, y:p.y, reg:true});
		cursor.graphics.s("white").mt(-25,0).lt(25,0).mt(0,-25).lt(0,20);
		cursor.compositeOperation = "difference";
		cursor.expand(0);
		zim.drag({obj:cursor});
		zog("place cursor to get new registration point location");
		stage.on("stagemouseup", report);
		return obj;
	}//-49.5

// SUBSECTION SHORT CHAINABLE

/*--
obj.pos = function(x, y, right, bottom, container, index, add, reg, regX, regY)

pos
zim DisplayObject method

DESCRIPTION
Chainable convenience function to position an object and optionally add to a container
** previously positioned only registration point
** now positions based on sides, top or bottom unless reg is set to true
By default, will position left and top of object - can also position right or bottom
Setting reg (or regX, regY) to true will position to the registration point
See: POSREG constant - set to true to change default pos() to reg=true

EXAMPLE
// 1. adds circle to default stage moves left and top of circle to 100, 100
circle1.pos(100, 100);

// 2. adds circle to box and positions right and bottom of circle 100 and 200
// from right and bottom of box bounds
circle2.pos(100, 200, true, true, box);

// 3. adds to stage and puts registration point at x=200 and y=0
circle3.pos({x:200, reg:true});

// 4. adds to stage and puts registration point at x=200 and y=radius (not y=0)
circle4.pos({x:200, regX:true});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
x - (default null or 0 for right) the x distance in the container to the left or right side of the object
	which side, depends on the right parameter
	if reg or regX is true then it is the distance to the registration point not the side
y - (default null or 0 for bottom) the y distance in the container to the top or bottom of the object
	which one, depends on the bottom parameter
	if reg or regY is true then it is the distance to the registration point not the top or bottom
right - (default false) set to true to postion the right side from the right of the container bounds
	if reg or regX is true then it is the distance to the registration point not the side
bottom - (default false) set to true to postion the bottom of the object from the bottom of the container bounds
	if reg or regY is true then it is the distance to the registration point not the bottom
container - (default current container or zimDefaultFrame stage) the Container to add the obj to and position
index - (default null) if provided will addChildAt the object at the index (0 being bottom)
add - (default true) add to container - set to false to not add
reg - (default false) set to true to position to the registration point rather than sides, top or bottom
	See: POSREG contant - set POSREG=true; to change the default pos() to position the registration point
	This is good to adjust legacy code - or if the original pos setting is preferred.
regX - (default reg) set to true to position x to the registration point rather than the sides
	will override reg if provided
regY - (default reg) set to true to position y to the registration point rather than the top or bottom
	will override reg if provided

RETURNS obj for chaining
--*///+41.5
	zim.pos = function(obj, x, y, right, bottom, container, index, add, reg, regX, regY) {
		var sig = "obj, x, y, right, bottom, container, index, add, reg, regX, regY";
		var duo; if (duo = zob(zim.pos, arguments, sig)) return duo;
		z_d("41.5");

		var PR = zim.POSREG;
		if (typeof POSREG !== typeof undefined) PR = POSREG;
		if (zot(reg)) reg = PR;

		if (zot(obj)) return;
		if (zot(reg) && zot(regX)) regX = false; // regX and regY override reg
		if (zot(reg) && zot(regY)) regY = false;
		if (!zot(reg) && zot(regX)) regX = reg;
		if (!zot(reg) && zot(regY)) regY = reg;
		if (right!==true) right = false;
		if (bottom!==true) bottom = false;

		if (zot(add)) add = true;
		if (!zot(container) && !container.addChild) container = null;
		if (!obj.getBounds && !object.getBounds()) {regX = regY = false;}

		if (zot(container)) {
			if (obj.parent) container = obj.parent;
			else if (zimDefaultFrame) container = zimDefaultFrame.stage;
		}

		if (!obj.parent && zot(x)) x = 0;
		if (!obj.parent && zot(y)) y = 0;

		if (!zot(container) && container.addChild && add) {
			if (zot(index) || (typeof index === 'number' && isNaN(index))) {
                if (!container.contains(obj)) container.addChild(obj);
            } else {
                container.addChildAt(obj, index);
            }
		}

		if (obj.parent && obj.parent.getBounds) {
			container = obj.parent;

			if (zot(x) && right==true) x = 0;
			if (zot(y) && bottom==true) y = 0;

			if (!zot(x)) obj.x = x;
			if (!zot(y)) obj.y = y;
			if (zot(x) && zot(y) && !right && !bottom) return obj;

			var shiftX = 0;
			var shiftY = 0;
			if (obj.getBounds()) {
				// var projected = obj.getTransformedBounds(); // copy of CreateJS version but without setting bounds
				var projected = getTransformedBounds(); // copy of CreateJS version but without setting bounds
				function getTransformedBounds () {
					var bounds = obj.getBounds()
					var x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height, mtx = obj._props.matrix;
					mtx = obj.getMatrix(mtx);
					if (x || y) { mtx.appendTransform(0,0,1,1,0,0,0,-x,-y); }
					var x_a = width*mtx.a, x_b = width*mtx.b;
					var y_c = height*mtx.c, y_d = height*mtx.d;
					var tx = mtx.tx, ty = mtx.ty;
					var minX = tx, maxX = tx, minY = ty, maxY = ty;
					if ((x = x_a + tx) < minX) { minX = x; } else if (x > maxX) { maxX = x; }
					if ((x = x_a + y_c + tx) < minX) { minX = x; } else if (x > maxX) { maxX = x; }
					if ((x = y_c + tx) < minX) { minX = x; } else if (x > maxX) { maxX = x; }
					if ((y = x_b + ty) < minY) { minY = y; } else if (y > maxY) { maxY = y; }
					if ((y = x_b + y_d + ty) < minY) { minY = y; } else if (y > maxY) { maxY = y; }
					if ((y = y_d + ty) < minY) { minY = y; } else if (y > maxY) { maxY = y; }
					return {x:minX, y:minY, width:maxX-minX, height:maxY-minY};
				}
				if (!zot(x)) {
					if (!right) {
						var local = new zim.Point(projected.x, projected.y);
					} else {
						var local = new zim.Point(projected.x+projected.width, projected.y+projected.height);
					}
					shiftX = local.x-x;
				}
				if (!zot(y)) {
					if (!bottom) {
						var local = new zim.Point(projected.x, projected.y);
					} else {
						var local = new zim.Point(projected.x+projected.width, projected.y+projected.height);
					}
					shiftY = local.y-y;
				}

				var b = container.getBounds();
				if (!zot(x)) {
					if (right) {
						if (regX) obj.x = b.width+b.x-x;
						else obj.x = b.width+b.x-x-shiftX;
					} else {
						if (regX) obj.x = x;
						else obj.x = x-shiftX;
						obj.x += b.x;
					}
				}
				if (!zot(y)) {
					if (bottom) {
						if (regY) obj.y = b.height+b.y-y;
						else obj.y = b.height+b.y-y-shiftY;
					} else {
						if (regY) obj.y = y;
						else obj.y = y-shiftY;
						obj.y += b.y;
					}
				}
			}
			// obj.x+=b.x*2;
			// obj.y+=b.y*2;
		}
		return obj;
	}//-41.5

/*--
obj.loc = function(target|x, y, container, index, add, localToLocal)

loc
zim DisplayObject method

DESCRIPTION
locates obj registration at registration of provided target
or at x and y if numbers are provided
This is like the original pos() where the registration is used
But it a single object with x y properties can be passed as a parameter
Will calculate localToLocal if target is provided
Also can add to container at an index

EXAMPLE
var circle1 = new Circle().pos(100,100);
var circle2 = new Circle().loc(circle1); // adds on top of circle1
END EXAMPLE

EXAMPLE
new Circle().loc(400, 400); // places reg of circle at 400, 400
// note, this is different than pos(400, 400) which would place left top of circle at 400, 400
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
target | x - (default null) an object with x and y properties such as any ZIM Display Object (Rectangle, Button, etc)
	or a zim Point(100, 100) object or {x:100, y:300}, etc.
	or can be a Number for an x value - in which case, a y value might be expected too
y - (default null) an optional y value
container - (default current container or zimDefaultFrame stage) the Container to add the obj to and position
index - (default null) if provided will addChildAt the object at the index (0 being bottom)
add - (default true) add to container - set to false to not add
localToLocal - (default true) if target is provided, the obj will be placed on the target matching locations across coordiate spaces

RETURNS obj for chaining
--*///+41.55
	zim.loc = function(obj, target, y, container, index, add, localToLocal, x) {
		var sig = "obj, target, y, container, index, add, localToLocal, x";
		var duo; if (duo = zob(zim.loc, arguments, sig)) return duo;
		z_d("41.55");
		if (zot(obj)) return;
		if (typeof target == "number") {
			x = target;
			target = null;
		}
		if (!zot(target)) {
			if (!zot(target.x)) x = target.x;
			if (!zot(target.y)) y = target.y;
		}
		if (zot(x) && zot(target)) x = obj.x;
		if (zot(y) && zot(target)) y = obj.y;

		if (zot(add)) add = true;
		if (zot(container)) {
			if (obj.parent) container = obj.parent;
			else if (zimDefaultFrame) container = zimDefaultFrame.stage;
		}
		if (add && container) {
			if (!obj.parent || container != obj.parent) {
				if (zot(index) || isNaN(index)) {container.addChild(obj);}
				else {container.addChildAt(obj, index);}
			}
		}
		if (zot(localToLocal)) localToLocal = true;
		if (target && target.parent && obj.parent && localToLocal) {
			var point = target.parent.localToLocal(x, y, obj.parent)
			x = point.x;
			y = point.y;
		}
		obj.x = x;
		obj.y = y;
		return obj;
	}//-41.55

/*--
obj.mov = function(x, y)

mov
zim DisplayObject method

DESCRIPTION
Move the object over in the x and/or y
Equivalant to obj.x += x and obj.y += y
Pass in 0 for no shift in x if you just want to shift y
Gives chainable relative position

EXAMPLE
var circle = new Circle().center(stage).mov(50); // move to right 50
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
x - (default 0) the distance in x to move (can be negative)
y - (default 0) the distance in y to move (can be negative)

RETURNS obj for chaining
--*///+41.6
	zim.mov = function(obj, x, y) {
		var sig = "obj, x, y";
		var duo; if (duo = zob(zim.mov, arguments, sig)) return duo;
		z_d("41.6");
		if (zot(obj)) return;
		if (!zot(x)) obj.x += x;
		if (!zot(y)) obj.y += y;
		return obj;
	}//-41.6

/*--
obj.sca = function(scale, scaleY)

sca
zim DisplayObject method

DESCRIPTION
Chainable convenience function to do scaleX and scaleY in one call.
If you pass in just the scale parameter, it scales both x and y to this value.
If you pass in scale and scaleY then it scales x and y independently.
Also see scaleTo(), fit() and Layout().

EXAMPLE
circle.sca(.5); // x and y scale to .5
circle.sca(.5, 2); // x scale to .5 and y scale to 2
END EXAMPLE

PARAMETERS
scale - the scale (1 being full scale, 2 being twice as big, etc.)
scaleY - (default null) pass this in to scale x and y independently

RETURNS obj for chaining
--*///+41.97
	zim.sca = function(obj, scale, scaleY) {
		z_d("41.97");
		if (zot(obj) || zot(obj.scaleX)) return;
		if (zot(scale)) scale = obj.scaleX;
		if (zot(scaleY)) scaleY = scale;
		obj.scaleX = scale; obj.scaleY = scaleY;
		return obj;
	}//-41.97

/*--
obj.alp = function(alpha)

alp
zim DisplayObject method

DESCRIPTION
Chainable convenience function to set the alpha
See also the CreateJS set({prop:val, prop2:val}) method;

EXAMPLE
circle.alp(.5);
END EXAMPLE

PARAMETERS
alpha - (default null) the alpha between 0 and 1

RETURNS obj for chaining
--*///+41.7
	zim.alp = function(obj, alpha) {
		z_d("41.7");
		if (zot(obj)) return;
		if (!zot(alpha)) obj.alpha = obj.hovOriginal = alpha;
		return obj;
	}//-41.7

/*--
obj.hov = function(value, prop)

hov
zim DisplayObject method

DESCRIPTION
Chainable convenience function to set the rollover (hover) property of an object
This defaults to alpha if a number and color if a string
But the type of property can also be set - for multiple properties, use a Button
This sets mouseover and mouseout events on the object
It will also set the cursor of the object to "pointer"
This can be changed with the cursor property or the cur() method

EXAMPLE
circle.alp(.5).hov(.8);
new Rectangle(100,100,blue).hov(green);
triangle.hov(1.5, scale);
END EXAMPLE

PARAMETERS
value - (default 1) the hover value of the property
	if a number, the default property is alpha
	if a string, the default property is color
	passing in -1 will remove the hover
prop - (default alpha or color as stated above) the property to change to the value on hover

RETURNS obj for chaining
--*///+41.75
	zim.hov = function(obj, value, type) {
		z_d("41.75");
		if (zot(obj)) return;
		if (zot(value)) value = 1;
		if (zot(type)) type = (typeof value == "number") ? "alpha" : "color";
		if (obj.hovMouseover) {
			obj.off("mouseover", obj.hovMouseover);
			obj.off("mouseout", obj.hovMouseout);
		}
		if (value == -1) {
			obj.cur("default");
			if (!zot(obj.hovOld)) obj[obj.hovType] = obj.hovOld;
			obj.hovOld = null;
			if (obj.stage) obj.stage.update();
			return obj;
		}
		obj.cur();
		obj.hovOld = obj[type];
		obj.hovNew = value;
		obj.hovType = type;
		obj.hovMouseover = obj.on("mouseover", function () {
			obj[type] = obj.hovNew;
			obj.stage.update();
		});
		obj.hovMouseout = obj.on("mouseout", function () {
			obj[type] = obj.hovOld;
			obj.stage.update();
		});
		return obj;
	}//-41.75

/*--
obj.rot = function(rotation)

rot
zim DisplayObject method

DESCRIPTION
Chainable convenience function to set the rotation
See also the CreateJS set({prop:val, prop2:val}) method;

EXAMPLE
rect.rot(180);
END EXAMPLE

PARAMETERS
rotation - (default null) the rotation in degrees

RETURNS obj for chaining
--*///+41.8
	zim.rot = function(obj, rotation, x, y) {
		z_d("41.8");
		if (zot(obj)) return;
		if (!zot(rotation)) {
			if (obj.parent && (!zot(x) || !zot(y))) {
				if (zot(x)) x = obj.x;
				if (zot(y)) y = obj.y;
				var diffX = x-obj.x;
				var diffY = y-obj.y;
				var inside = obj.parent.localToLocal(x, y, obj);
				var outside = new zim.Point(obj.x, obj.y);
				obj.reg(inside.x, inside.y);
				obj.rotation=rotation;
				obj.x = x;
				obj.y = y;
				var backPoint = obj.parent.localToLocal(outside.x, outside.y, obj);
				obj.reg(backPoint.x, backPoint.y);
				obj.x = outside.x;
				obj.y = outside.y;
			} else {
				obj.rotation=rotation;
			}
		}
		return obj;
	}//-41.8

/*--
obj.siz = function(width, height, only)

siz
zim DisplayObject method

DESCRIPTION
Chainable convenience function to set width and height in one call.
If you pass in just the width or height parameter, it keeps the aspect ratio.
If you want to set only the width or height, then set only to true.
If you pass in both the width and height then it sets both.
Note: that width and height will adjust the scaleX and scaleY of the object.
Also see width, height, widthOnly, heightOnly.

EXAMPLE
var rect = new Rectangle(100,200,blue).addTo(stage);
rect.siz(200); // sets width to 200 and height to 400
rect.siz(200, null, true); // sets width to 200 and leaves height at 200
rect.siz(200, 100); // sets width to 200 and height to 100
END EXAMPLE

PARAMETERS
width - (default null) the width of the object
	setting only the width will set the widht and keep the aspect ratio
	unless the only parameter is set to true
height - (default null) the height of the object
	setting only the width will set the widht and keep the aspect ratio
	unless the only parameter is set to true
only - (default false) - defaults to keeping aspect ratio when one dimension set
 	set to true to scale only a single dimension (like widthOnly and heightOnly properties)

RETURNS obj for chaining
--*///+41.85
	zim.siz = function(obj, width, height, only) {
		z_d("41.85");
		if (zot(obj)) return;
		if (zot(only)) only = false;
		if (!zot(width) && !zot(height)) {
			obj.widthOnly = width; obj.heightOnly = height;
		} else if (!zot(width)) {
			if (only) {obj.widthOnly = width;} else {obj.width = width;}
		} else if (!zot(height)) {
			if (only) {obj.heightOnly = height;} else {obj.height = height;}
		}
		return obj;
	}//-41.85

/*--
obj.ske = function(skewX, skewY)

ske
zim DisplayObject method

DESCRIPTION
Chainable convenience function to skewX and skewY (slant)
See also the CreateJS set({prop:val, prop2:val}) method;

EXAMPLE
circle.ske(20);
END EXAMPLE

PARAMETERS
skewX - (default null) the x skew
skewY - (default null) the y skew

RETURNS obj for chaining
--*///+41.9
	zim.ske = function(obj, skewX, skewY) {
		z_d("41.9");
		if (zot(obj)) return;
		if (!zot(skewX)) obj.skewX = skewX;
		if (!zot(skewY)) obj.skewY = skewY;
		return obj;
	}//-41.9

/*--
obj.reg = function(regX, regY, still)

reg
zim DisplayObject method

DESCRIPTION
Chainable convenience function to regX and regY (registration point)
The registration point is the point the object is positioned with
and the object scales and rotates around the registration point
See also the CreateJS set({prop:val, prop2:val}) method;
See also centerReg()

EXAMPLE
circle.reg(200, 200);
END EXAMPLE

PARAMETERS
regX - (default null) the x registration
regY - (default null) the y registration
still - (default false) set to true to move the object to counter the registration change
	this will let you set the registration point without moving the object

RETURNS obj for chaining
--*///+41.95
	zim.reg = function(obj, regX, regY, still) {
		z_d("41.95");
		if (zot(obj)) return;
		var lastRegX = obj.regX;
		var lastRegY = obj.regY;
		if (!zot(regX)) obj.regX = regX;
		if (!zot(regY)) obj.regY = regY;
		if (still && obj.parent) {
			var p = obj.localToLocal(lastRegX, lastRegY, obj.parent);
			obj.x -= p.x-obj.x;
			obj.y -= p.y-obj.y;
		}
		return obj;
	}//-41.95

/*--
obj.top = function()

top
zim DisplayObject method

DESCRIPTION
Places object on top layer in container - if in a container
returns the object for chaining
Can also just addTo(container) to re-add to top
NOTE for some situations this will break code
so a safer way is obj.parent.setChildIndex(obj, obj.parent.numChildren-1);
which is what this method uses

EXAMPLE
circle.top(); // brings circle to the top of parent container
END EXAMPLE

RETURNS obj for chaining
--*///+41.62
	zim.top = function(obj) {
		z_d("41.62");
		if (zot(obj) || !obj.parent) return;
		obj.parent.setChildIndex(obj, obj.parent.numChildren-1);
		return obj;
	}//-41.62

/*--
obj.bot = function()

bot
zim DisplayObject method

DESCRIPTION
Places object on bottom layer in container - if in a container
returns the object for chaining

EXAMPLE
circle.bot(); // put circle on bottom layer (0) of parent container
END EXAMPLE

RETURNS obj for chaining
--*///+41.63
	zim.bot = function(obj) {
		z_d("41.63");
		if (zot(obj) || !obj.parent) return;
		obj.parent.setChildIndex(obj, 0);
		return obj;
	}//-41.63

//
/*--
obj.ord = function(num)

ord
zim DisplayObject method

DESCRIPTION
Moves object layer order in container - if in a container
returns the object for chaining

EXAMPLE
circle.bot().ord(1); // put circle one layer up from bottom of parent container
circle.top().ord(-2); // put circle two layers down from top (third highest layer)
END EXAMPLE

PARAMETERS
num - (default 0) the number of levels (layers) up or down to move the object
	1 will move the object one level higher, -1 will move it one level lower
	2 will move the object two levels higher, -2 will be two levels lower, etc.

RETURNS obj for chaining
--*///+41.64
	zim.ord = function(obj, num) {
		z_d("41.64");
		if (zot(obj) || !obj.parent) return;
		if (zot(num)) num = 0;
		obj.parent.setChildIndex(obj, obj.parent.getChildIndex(obj)+num);
		return obj;
	}//-41.64

/*--
obj.cur = function(type)

cur
zim DisplayObject method

DESCRIPTION
Chainable function that sets the object's cursor to the type provided - same as CSS cursors

EXAMPLE
var circle = new Circle(10, "red").center(stage).cur(); // "pointer"
circle.on("click", function(){zog("yes");});
END EXAMPLE

PARAMETERS
type - (default "pointer") the CSS cursor to set
	https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
	Common cursors are "default", "pointer", "Wait", "move", "grab", "grabbing", "zoom-in", "zoom-out", and various resize like "ew-resize"

RETURNS obj for chaining
--*///+41.1
	zim.cur = function(obj, type) {
		z_d("41.1");
		if (zot(obj)) {zog("zim methods - cur(): please provide object"); return obj;}
		if (zot(type)) type = "pointer";
		obj.cursor = type;
		return obj;
	}//-41.1

/*--
obj.sha = function(color||Shadow, offsetX, offsetY, blur)

sha
zim DisplayObject method

DESCRIPTION
Chainable function that sets the object's drop shadow to a CreateJS Shadow indirectly or directly

EXAMPLE
// indirectly set the CreateJS Shadow
// with sha(color, offsetX, offsetY, blur)
var circle = new Circle(10, "red").center(stage).sha("rgba(0,0,0,.2)", 10, 5, 5);

// directly set the CreateJS Shadow
// with sha(new createjs.Shadow())
var shadow = new createjs.Shadow("rgba(0,0,0,.2)", 10, 5, 5);
var circle1 = new Circle(10, "pink").center(stage).mov(-30).sha(shadow);
var circle2 = new Circle(10, "yellow").center(stage).mov(30).sha(shadow);
END EXAMPLE

PARAMETERS
color||Shadow (default "rgba(0,0,0,.3)") the CSS color for the shadow - "red", dark, etc.
	or pass a single parameter that is a CreateJS Shadow object https://www.createjs.com/docs/easeljs/classes/Shadow.html
offsetX (default .08 the width or 5 if no width) the distance in the x that the shadow is moved over - can be negatitve
offsetY (default .08 the height or 5 if no height) the distance in the y that the shadow is moved over - can be negatitve
blur (default .16 the width or 10 if no width) the distance the shadow is blurred

RETURNS obj for chaining
--*///+41.2
	zim.sha = function(obj, color, offsetX, offsetY, blur) {
		z_d("41.2");
		if (zot(obj)) {zog("zim methods - sha(): please provide object"); return obj;}
		if (zot(color)) color = "rgba(0,0,0,.3)";
		if (color.blur) {obj.shadow = color; return obj;} // a Shadow is passed as first parameter
		if (zot(offsetX)) offsetX = obj.width ? obj.width *.08 : 5;
		if (zot(offsetY)) offsetY = obj.height ? obj.height *.08 : 5;
		if (zot(blur)) blur = obj.width ? obj.width *.16 : 10;
		obj.shadow = new createjs.Shadow(color, offsetX, offsetY, blur);
		return obj;
	}//-41.2

/*--
obj.dep = function(depth)

dep
zim DisplayObject method

DESCRIPTION
A chainable method to set the depth property of a Display object for use with ZIM VR().
When the object is added to VR it will be cloned into two channels and shifted left and right based on its depth.
A depth of 0 will not shift the object and this will appear flat on the screen.
A depth of 20 will shift 20 pixels left and right and appear to come out of the screen.
A depth of -20 will appear to go into the screen.
Depending on the VR parallax settings, depth can also affect parallax.
A negative depth does not unless negativeParallax is set to true in the VR parameters.

EXAMPLE
var circle = new Circle().center(stage).dep(10);
END EXAMPLE

PARAMETERS
depth - (default 0) the apparent depth in ZIM VR - or set the depth property

RETURNS obj for chaining
--*///+41.65
	zim.dep = function(obj, depth) {
		z_d("41.65");
		if (zot(obj)) return;
		if (zot(depth)) depth = 0;
		obj.depth = depth;
		return obj;
	}//-41.65

// SUBSECTION TAP, CHANGE, DRAG, TRANSFORM, GESTURE, PHYSICS

/*--
obj.tap = function(call, distance, time, once)

tap
zim DisplayObject method

DESCRIPTION
Chainable convenience method that adds a mousedown and mouseup event to the object
that requires the to move less than distance parameter
This is more like a proper click - down up without dragging.
This method works on desktop or mobile, etc.
An optional time parameter is provided if a minimum time is desired.
Note that a click event also works on mobile as a "tap"
but click also allows dragging between down and up presses - so really is a mouseup.
This will automatically add a cursor of "pointer"
which can be changed with the cursor property or cur() method

NOTE: tap() ignores List titleBar and organizer as to not conflict with tapping on actual list

SEE: noTap() as well

EXAMPLE
new Circle(50, "red").cur().tap(function (e) {
	e.target.alpha = .5;
	stage.update();
});
END EXAMPLE

PARAMETERS
call - the function to call when clicked
	call will receive the event object as a parameter (with target, currentTarget, etc. properties)
distance - (default 5) distance in pixels within which the mouseup must occur for a click to be counted
time - (default 10000) time in ms within which the mouseup must occur for a click to be counted
once - (default false) set to true to capture only one click then auto-remove listeners

RETURNS obj for chaining
--*///+47.8
	zim.tap = function(obj, call, distance, time, once) {
		z_d("47.8");
		if (zot(obj) || zot(call) || typeof call != "function") return;
		if (zot(distance)) distance = 5;
		if (zot(time)) time = 10000;
		if (zot(once)) once = false;
		obj.cursor = "pointer";
		var stage;
		obj.zimClickDownEvent = obj.on("mousedown", function (e) {
			stage = e.target.stage;
			if (e.currentTarget.type == "List") {
				if (e.target.type == "WindowBacking") return;
	            var local = e.currentTarget.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
	            if (local.y <= 0) return; // avoid titleBar and organizer
	        }
	        var lastX = e.stageX/stage.scaleX;
	        var lastY = e.stageY/stage.scaleY;
			var startTime = Date.now();
	        obj.zimClickUpEvent = obj.on("pressup", function (e) {
	            if (Math.abs(lastX+lastY-e.stageX/stage.scaleX-e.stageY/stage.scaleY) < distance && Date.now()-startTime < time) {
					if (obj.excludeTap) return;
					call(e);
	            }
	        }, null, true); // just once
	    }, null, once);
		return obj;
	}//-47.8

/*--
obj.noTap = function()

noTap
zim DisplayObject method

DESCRIPTION
removes the mousedown and mouseup events added with tap()

EXAMPLE
var circle = new Circle(50, "red").cur().tap(function (e) {
	e.target.alpha = .5;
	stage.update();
});
if (score > 10) circle.noTap();
END EXAMPLE

RETURNS obj for chainging
--*///+47.9
	zim.noTap = function(obj) {
		z_d("47.9");
		if (zot(obj)) return;
		if (obj.cursor) obj.cursor = "default";
		obj.off("mousedown", obj.zimClickDownEvent);
		obj.off("pressup", obj.zimClickUpEvent);
		return obj;
	}//-47.9


/*--
obj.change = function(call, once)

change
zim DisplayObject method

DESCRIPTION
Chainable convenience method that adds a change event to the object
This only works for components that dispatch a change event ;-)

SEE: noChange() as well

EXAMPLE
new Tabs().change(function (e) {
	zog(e.target.text); // the text of the selected tab
});
END EXAMPLE

PARAMETERS
call - the function to call when changed
	call will receive the event object as a parameter (with target, currentTarget, etc. properties)
once - (default false) set to true to capture only one click then auto-remove listeners

RETURNS obj for chaining
--*///+47.85
	zim.change = function(obj, call, once) {
		z_d("47.85");
		if (zot(obj) || zot(call) || typeof call != "function") return;
		if (zot(once)) once = false;
		obj.zimChangeEvent = obj.on("change", function (e) {
	        call(e);
	    }, null, once);
		return obj;
	}//-47.85

/*--
obj.noChange = function()

noChange
zim DisplayObject method

DESCRIPTION
removes the change event added with change()

EXAMPLE
var tabs = new Tabs().change(function (e) {
	zog(e.target.text);
});
if (score > 10) tabs.noChange(); // removes change event
END EXAMPLE

RETURNS obj for chainging
--*///+47.95
	zim.noChange = function(obj) {
		z_d("47.95");
		if (zot(obj)) return;
		obj.off("mousedown", obj.zimChangeEvent);
		return obj;
	}//-47.95

/*--
obj.drag = function(boundary, overCursor, dragCursor, all, swipe, localBounds, onTop, surround, slide, slideDamp, slideSnap, reg, removeTweens, startBounds, rect, currentTarget, offStage)

drag
zim DisplayObject method

DESCRIPTION
Adds drag and drop to an object with a variety of options.
Handles scaled, rotated nested objects.

EXAMPLE
var radius = 50;
var circle = new Circle(radius, "red");
circle.center(stage);
circle.drag();

// OR with chaining
var circle = new Circle(radius, "red").center(stage).drag();

// OR with ZIM DUO
circle.drag({slide:true});

// BOUNDARY
// circle has its registration point in the middle
// keep registration point within rectangle starting at x=100, y=100
// and drag within a width of 500 and height of 400
var boundary = new Boundary(100,100,500,400);
circle.drag(boundary);

// or keep circle on the stage with the following
var boundary = new Boundary(0,0,stageW,stageH).contract(radius);
circle.drag(boundary); // drag within stage
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
boundary - (default null) a ZIM Boundary object for the drag boundary
	if surround is true then it will make sure the obj surrounds the boundary rather than stays within it
	this boundary is relative to the stage (global)
	if a boundary relative to the object's parent is desired then set the localBounds parameter to true
overCursor - (default "pointer") the CSS cursor property as a string for rolling over the object
dragCursor - (default "pointer") the CSS cursor property as a string for pressing and dragging the object
all - (default false) set to true to drag a whole container rather than its parts (was called currentTarget)
	eg. container.drag(); will drag any object within the container
	container.drag({all:true}) will drag the whole container
	See: DRAGALL contant - set DRAGALL=true; to change the default drag to drag a whole container
swipe - (default false) which prevents a swipe from triggering when dragging
localBounds - (default false) which means the rect is global - set to true for a rect in the object parent frame
onTop - (default true) brings the dragged object to the top of the container - unless Keyboard at top.
	// to drag on top of keyboard, set the type property of the keyboard to "LowerKeyboard" - or anything other than Keyboard
surround - (default false) is for dragging a big object that always surrounds the boundary
slide - (default false) will let you throw the object and dispatch a slidestop event when done
slideDamp - (default .3) is the damping setting for the slide (1 is no damping and .1 will slide more, etc.)
slideSnap - (default true) lets the object go outside and snap back to bounds - also "vertical", "horizontal", and false
reg - (default false) when set to true will snap the registration of the object to the mouse position
removeTweens - (default true) will automatically remove tweens from dragged object unless set to false
startBounds - (default true) set to false to ignore bound rect before dragging (sometimes handy when putting drag on container)
rect - (depreciated) same as boundary - kept for backwards compatibility when using config object
currentTarget - (default false) same as the all parameter - kept for backwards compatibility when using config object
offStage - (default false) set to true to be able to drag object off stage (thanks Shammi!)

note: will not update stage if OPTIMIZE is set to true
unless Ticker.update is set to true or you run Ticker.always(stage) see Ticker

EVENTS
Adds a "slidestop" event to the drag object that is dispatched when the object comes to rest after sliding

RETURNS obj for chaining
--*///+31
	zim.drag = function(obj, boundary, overCursor, dragCursor, all, swipe, localBounds, onTop, surround, slide, slideDamp, slideSnap, reg, removeTweens, startBounds, rect, currentTarget, offStage) {
		var sig = "obj, boundary, overCursor, dragCursor, all, swipe, localBounds, onTop, surround, slide, slideDamp, slideSnap, reg, removeTweens, startBounds, rect, currentTarget, offStage";
		var duo; if (duo = zob(zim.drag, arguments, sig)) return duo;
		z_d("31");

		if (zot(obj) || !obj.on) return;

		var DA = zim.DRAGALL;
		if (typeof DRAGALL !== typeof undefined) DA = DRAGALL;
		if (zot(all) && zot(currentTarget)) all = DA;

		if (obj.zimDown) obj.noDrag(); // clear any previous drags
		obj.cursor = (zot(overCursor)) ? "pointer" : overCursor;
		if (zot(all) && zot(currentTarget)) currentTarget = false;
		else if (!zot(all)) currentTarget = all;
		else if (zot(currentTarget)) currentTarget = false;
		if (obj.type == "Tag" || obj.type == "TextArea" || obj.type == "Loader") currentTarget = true;
		if (zot(swipe)) swipe = false;
		if (zot(localBounds)) localBounds = false;
		if (zot(onTop)) onTop = true;
		if (zot(surround)) surround = false;
		if (zot(slide)) slide = false;
		if (zot(slideDamp)) slideDamp = .3;
		if (zot(slideSnap)) slideSnap = true;
		var snapOptions = ["horizontal", "vertical", "auto"];
		if (slideSnap !== true && snapOptions.indexOf(slideSnap) < 0) slideSnap = false;
		if (zot(reg)) reg = false;
		if (zot(removeTweens)) removeTweens = true;
		if (zot(startBounds)) startBounds = true;
		if (zot(boundary) && !zot(rect)) boundary = rect;

		if (slide) {
			// set up damping for slide and variables used to predict future locations
			// these get populated in a setTimeout in the init function
			var dampX;
			var dampY;
			var back; // how many ticks ago to estimate trajectory
			var lastCount;
			var backX;
			var backY;
			var upX; // mouse up translated to local
			var upY;
			var objUpX; // drag object x when mouse up
			var objUpY;
			var lastBackX; // used to calculate trajectory
			var lastBackY;
			var lastX; // used to see if sliding object is still moving
			var lastY;
			var hasMoved;
		}

		zim.setSwipe(obj, swipe);
		obj.zimBoundary = boundary;
		obj.zimLocalBounds = localBounds;
		var downCheck = false;
		var moveCheck = false;

		var diffX; var diffY; var point; var r;	var rLocal;
		obj.zimAdded = obj.on("added", initializeObject, null, true); // if not added to display list
		obj.zimRemoved = obj.on("removed", unInitializeObject, null, true);
		if (obj.parent) initializeObject();

		function initializeObject() {
			obj.off("added", obj.zimAdded);
			// check position right away if there is a bounding box
			// there is no mousedown so set the diffX and diffY to 0
			diffX = 0; diffY = 0;
			// positionObject() is used as well in the dragmove function
			// where it expects a global x and y
			// so convert obj.x and obj.y positions inside its parent to global:
			if (obj.zimBoundary) {
				if (localBounds) {
					r = zim.boundsToGlobal(obj.parent, obj.zimBoundary);
					if (surround) rLocal = obj.zimBoundary;
				} else {
					r = obj.zimBoundary;
					if (surround) rLocal = zim.boundsToGlobal(obj.parent, obj.zimBoundary, true); // flips to global to local
				}
			}
			if (r && startBounds) {
				point = obj.parent.localToGlobal(obj.x, obj.y);
				positionObject(obj, point.x, point.y);
			}
			if (slide) {

				// little delay to in case position is set directly after drag is called
				setTimeout(function() {
					// set up damping for slide and variables used to predict future locations
					dampX = new zim.Damp(obj.x, slideDamp);
					dampY = new zim.Damp(obj.y, slideDamp);
					obj.zimDragImmediate = function(x, y) {
						objUpX = x;
						objUpY = y;
						upX = lastBackX = upY = lastBackY = 0;
						if (!zot(x)) dampX.immediate(x);
						if (!zot(y)) dampY.immediate(y);
					}
					back = 3; // how many ticks ago to estimate trajectory
					lastCount = 0;
					backX = [];
					backY = [];
					upX = obj.x; // mouse up translated to local
					upY = obj.y;
					objUpX = obj.x; // drag object x when mouse up
					objUpY = obj.y;
					lastBackX = obj.x; // used to calculate trajectory
					lastBackY = obj.y;
					lastX = -10000; // used to see if sliding object is still moving
					lastY = -10000;
					obj.zimDragMoving = true;
					setUpSlide();
				}, 50);
			}
			if (!zot(obj.zimMaskDynamic)) obj.zimMaskApply(); // set mask set by zimMask to dynamic
		}

		function unInitializeObject() {
			if (obj.zimDragTicker) zim.Ticker.remove(obj.zimDragTicker);
		}

		var dragObject;

		var stage;
		obj.pointers = {};
		obj.zimDown = obj.on("mousedown", function(e) {
			if (!obj.stage) return;
			stage = obj.stage;
			obj.dragMouseX = Math.round(e.stageX/stage.scaleX);
			obj.dragMouseY = Math.round(e.stageY/stage.scaleY);
			var id = "id"+Math.abs(e.pointerID+1);
			obj.pointers[id] = true; // keep track of multitouch to keep object ticker alive
			// e.stageX and e.stageY are global
			// e.target.x and e.target.y are relative to e.target's parent
			// bring stageX and stageY into the parent's frame of reference
			// could use e.localX and e.localY but might be dragging container or contents
			dragObject = (currentTarget)?e.currentTarget:e.target;
			if (obj.zimBoundary && !dragObject.getBounds()) {zog("zim.drag() - drag object needs bounds set"); return;}
			downCheck = true;
			obj.stage.mouseMoveOutside = true;

			// add a function to the Ticker queue (remove it if there first)
			if (!slide) { // slide has a persistent Ticker function
				if (obj.zimDragTicker) zim.Ticker.remove(obj.zimDragTicker);
				obj.zimDragTicker = zim.Ticker.add(function(){}, stage);
			}
			if (removeTweens) createjs.Tween.removeTweens(dragObject);
			if (!dragObject.parent) return;
			if (onTop) {
				var nc = dragObject.parent.numChildren-1;
				if (dragObject.parent.getChildAt(nc).type=="Keyboard") nc--;
				dragObject.parent.setChildIndex(dragObject,nc);
				if (dragObject.ZIMoutlineShape) dragObject.outline();
			}
			if (dragObject.parent == obj.stage) var point = new zim.Point(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
			else var point = dragObject.parent.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
			if (reg) {
				dragObject.x = point.x;
				dragObject.y = point.y;
			}
			diffX = point.x - dragObject.x;
			diffY = point.y - dragObject.y;

			if (obj.zimBoundary) {
				if (localBounds) {
					r = zim.boundsToGlobal(dragObject.parent, obj.zimBoundary);
					if (surround) rLocal = obj.zimBoundary;
				} else {
					r = obj.zimBoundary;
					if (surround) rLocal = zim.boundsToGlobal(dragObject.parent, obj.zimBoundary, true); // true flips to global to local
				}
			}
			// just a quick way to set a default cursor or use the cursor sent in
			obj.cursor = (zot(dragCursor))?"pointer":dragCursor;

			// extra slide settings to project where the object will slide to
			if (slide) {
				obj.slideStartX = obj.x;
				obj.slideStartY = obj.y;
				hasMoved = false;

				lastCount = 0;
				backX = [point.x];
				backY = [point.y];
				lastX = -10000; // reset
				lastY = -10000;
				obj.zimDragMoving = true;
			}

			// extra pen drag
			if (obj.type == "Pen") {
				obj.zimDragCheck = true;
				moveCheck = false;
			}

		}, true);

		obj.zimMove = obj.on("pressmove", function(e) {
			if (!downCheck) return;
			var x = offStage?e.rawX:e.stageX;
			var y = offStage?e.rawY:e.stageY;
			obj.dragMouseX = Math.round(x/stage.scaleX);
			obj.dragMouseY = Math.round(y/stage.scaleY);
			positionObject(dragObject, x/stage.scaleX, y/stage.scaleY);
			if (dragObject.ZIMoutlineShape) dragObject.outline();
			if (obj.type == "Pen" && !moveCheck && obj.drawing) moveCheck = true;
			else if (obj.type == "Tag" || obj.type == "TextArea" || obj.type == "Loader") obj.resize();
		}, true);


		function positionObject(o, x, y) {

			if (zot(o)) o = (dragObject) ? dragObject : obj; // so zim.dragBoundary can use this

			// x and y are the desired global positions for the object o
			// checkBounds returns the same values if there are no bounds
			// and returns values inside the bounds if there are bounds set
			// or returns a position so that object o surrounds the bounds if surround is true
			// firstly, convert the global x and y to a point relative to the object's parent
			if (!o.parent) return;
			// if (!o.stage) return;

			if (zot(x) || zot(y)) {
				// so zim.dragBoundary can use this to position on change
				// it may be we are resizing before we even drag at all
				// so we need to establish variables that would have been made on drag events
				var p = o.parent.localToGlobal(o.x, o.y);
				diffX = diffY = 0;
				if (obj.zimBoundary) {
					if (localBounds) {
						r = zim.boundsToGlobal(o.parent, obj.zimBoundary);
						if (surround) rLocal = o.zimBoundary;
					} else {
						r = obj.zimBoundary;
						if (surround) rLocal = zim.boundsToGlobal(o.parent, obj.zimBoundary, true); // flips to global to local
					}
				}
				x = p.x;
				y = p.y;
				if (slide) {
					objUpX = o.x;
					objUpY = o.y;
					dragObject = o;
					if (dampX) dampX.immediate(objUpX);
					if (dampY) dampY.immediate(objUpY);
				}
			}
			var point = o.parent.globalToLocal(x, y);
			var checkedPoint;
			if (slide && slideSnap) {
				if (slideSnap == "vertical") {
					checkedPoint = checkBounds(o,point.x-diffX, point.y-diffY);
					o.x = checkedPoint.x;
					o.y = point.y-diffY;
				} else if (slideSnap == "horizontal") {
					checkedPoint = checkBounds(o,point.x-diffX, point.y-diffY);
					o.x = point.x-diffX;
					o.y = checkedPoint.y;
				} else {
					o.x = point.x-diffX;
					o.y = point.y-diffY;
				}
			} else {
				checkedPoint = checkBounds(o,point.x-diffX, point.y-diffY);
				// now set the object's x and y to the resulting checked local point
				o.x = checkedPoint.x;
				o.y = checkedPoint.y;
			}
			if (slide && !hasMoved && (o.slideStartX != o.x || o.slideStartY != o.y)) hasMoved = true;
		}
		obj.zimPosition = positionObject;

		obj.zimUp = obj.on("pressup", function(e) {
			var id = "id"+Math.abs(e.pointerID+1);
			delete obj.pointers[id];
			if (!downCheck) return;
			obj.cursor = (zot(overCursor))?"pointer":overCursor;
			if (slide) {
				if (dragObject.parent) {
					var x = offStage?e.rawX:e.stageX;
					var y = offStage?e.rawY:e.stageY;
					var point = dragObject.parent.globalToLocal(x/stage.scaleX, y/stage.scaleY);
				} else {
					var point = new zim.Point(0,0);
				}
				downCheck = false;
				upX = point.x;
				upY = point.y;
				objUpX = dragObject.x;
				objUpY = dragObject.y;
				dampX.immediate(dragObject.x);
				dampY.immediate(dragObject.y);
			} else {
				var pointerCount = 0;
				for (var o in obj.pointers) {
					pointerCount++;
				}
				if (pointerCount == 0) zim.Ticker.remove(obj.zimDragTicker);
				// extra pen drag
				if (obj.type == "Pen") {
					obj.zimDragCheck = false;
					if (moveCheck) obj.stopCheck();
				}
			}
			if (obj.stage) obj.stage.update();
		}, true);

		// the bounds check for registration inside the bounds
		// or if surround is set for the whole object staying outside the bounds
		function checkBounds(o, x, y) {
			if (!o.parent) return {x:x,y:y}
			if (r) {
				if (surround) {
					var w = o.getBounds().width;
					var h = o.getBounds().height;
					var bx = o.getBounds().x;
					var by = o.getBounds().y;
					if (w < rLocal.width) {
						// put half way between
						x = rLocal.x + (rLocal.width - w) / 2 + (o.regX-bx);
					} else {
						if (x - (o.regX-bx) > rLocal.x) {
							x = rLocal.x + (o.regX-bx);
						}
						if (x - (o.regX-bx) + w < rLocal.x + rLocal.width) {
							x = rLocal.x + rLocal.width + (o.regX-bx) - w;
						}
					}
					if (o.height < rLocal.height) {
						// put half way between
						y = rLocal.y + (rLocal.height - h) / 2 + (o.regY-by);
					} else {
						if (y - (o.regY-by) > rLocal.y) {
							y = rLocal.y + (o.regY-by);
						}
						if (y - (o.regY-by) + h < rLocal.y + rLocal.height) {
							y = rLocal.y + rLocal.height + (o.regY-by) - h;
						}
					}
				} else {
					// convert the desired drag position to a global point
					// note that we want the position of the object in its parent
					// so we use the parent as the local frame
					var point = o.parent.localToGlobal(x,y);
					// r is the bounds rectangle on the global stage
					// r is set during mousedown to allow for global scaling when in localBounds mode
					// if you scale in localBounds==false mode, you will need to reset bounds with dragBoundary()
					x = Math.max(r.x, Math.min(r.x+r.width, point.x));
					y = Math.max(r.y, Math.min(r.y+r.height, point.y));
					// now that the point has been checked on the global scale
					// convert the point back to the obj parent frame of reference
					point = o.parent.globalToLocal(x, y);
					x = point.x;
					y = point.y;
				}
			}
			return {x:x,y:y}
		}

		// we store where the object was a few ticks ago and project it forward
		// then damp until it stops - although the ticker keeps running and updating
		// if it snaps then the object is allowed to go past the bounds and damp back
		// if it is not snapping then the object stops at the bounds when it is slid
		function setUpSlide() {
			var stage = obj.stage;
			obj.zimDragTicker = function() {
				if (zot(obj.slideStartX)) return; // don't stop other things like window scrollbar from moving object

				if (!dragObject) dragObject = obj; // could be risky if intending to drag children
				if (downCheck) {
					if (dragObject.parent) {
						var point = dragObject.parent.globalToLocal(obj.dragMouseX, obj.dragMouseY);
					} else {
						var point = new zim.Point(0,0);
					}
					lastCount++;
					backX.push(point.x);
					backY.push(point.y);
					if (lastCount >= back) {
						lastBackX = backX.shift();
						lastBackY = backY.shift();
					} else {
						lastBackX = backX[0];
						lastBackY = backY[0];
					}
				} else {
					if (!obj.zimDragMoving) return;
					var desiredX = objUpX + upX-lastBackX;
					var desiredY = objUpY + upY-lastBackY;
					if (r) {
						var checkedPoint = checkBounds(dragObject, desiredX, desiredY);
						desiredX = checkedPoint.x;
						desiredY = checkedPoint.y;
					}
					if (!slideSnap) {
						var checkedPoint = checkBounds(dragObject, dampX.convert(desiredX), dampY.convert(desiredY));
						dragObject.x = checkedPoint.x;
						dragObject.y = checkedPoint.y;
						testMove(dragObject,dragObject.x,dragObject.y,dragObject.x,dragObject.y);
					} else {
						dragObject.x = dampX.convert(desiredX);
						dragObject.y = dampY.convert(desiredY);
						testMove(dragObject,dragObject.x,dragObject.y,desiredX,desiredY);
					}
				}
			}
			function testMove(o,x,y,desiredX,desiredY) {
				if (Math.abs(o.x-lastX) < .1 && Math.abs(o.y-lastY) < .1) {
					obj.zimDragMoving = false;
					o.x = desiredX; // snap to final resting place
					o.y = desiredY;
					o.slideStartX = null;
					o.slideStartY = null;
					if (hasMoved) o.dispatchEvent("slidestop");
					// extra pen drag
					if (obj.type == "Pen") {
						obj.zimDragCheck = false;
						if (moveCheck) obj.stopCheck();
					}
				} else {
					lastX = x;
					lastY = y;
				}
			}
			zim.Ticker.add(obj.zimDragTicker, stage);
		}
		return obj;
	}//-31

/*--
obj.noDrag = function()

noDrag
zim DisplayObject method

DESCRIPTION
Removes drag function from an object.
This is not a stopDrag function (as in the drop of a drag and drop).
Dropping happens automatically with the drag() function.
The noDrag function turns off the drag function so it is no longer draggable.

EXAMPLE
circle.noDrag();
END EXAMPLE

RETURNS obj for chaining
--*///+32
	zim.noDrag = function(obj) {
		z_d("32");
		if (zot(obj) || !obj.on) return;
		obj.cursor = "default";
		zim.setSwipe(obj, true);
		obj.off("added", obj.zimAdded);
		obj.off("removed", obj.zimRemoved);
		obj.off("mousedown", obj.zimDown);
		obj.off("pressmove", obj.zimMove);
		obj.off("pressup", obj.zimUp);
		if (zim.Ticker && obj.zimDragSlide) zim.Ticker.remove(obj.zimDragSlide);
		obj.zimDragMoving=obj.zimAdded=obj.zimRemoved=obj.zimDown=obj.zimMove=obj.zimUp=obj.zimBoundary=obj.zimDragSlide=null;
		return obj;
	}//-32

/*--
obj.dragBoundary = function(boundary)

dragBoundary
zim DisplayObject method

DESCRIPTION
Dynamically changes or adds a boundary rectangle to the object being dragged with drag().

NOTE: replaces old ZIM dragRect() method

EXAMPLE
var boundary = new Boundary(100,100,500,400);
circle.dragBoundary(boundary);
END EXAMPLE

PARAMETERS
boundary - is a ZIM Boundary object for the bounds - the local / global does not change from the original drag

RETURNS obj for chaining
--*///+33
	zim.dragBoundary = function(obj, boundary, immediate) {
		z_d("33");
		if (zot(obj) || !obj.on) return;
		if (zot(boundary)) return;
		obj.zimBoundary = boundary;
		obj.zimDragMoving = true;
		if (obj.zimPosition) obj.zimPosition();
		return obj;
	}//-33
	zim.dragRect = zim.dragBoundary; // backwards compatible

/*--
obj.transform = function(move, stretchX, stretchY, scale, rotate, allowToggle, visible, onTop, showStretch, showRotate, showScale, showReg, showBorder, borderColor, borderWidth, dashed, customCursors, handleSize, regSize, snapDistance, snapRotation, cache, events, ghostColor, ghostWidth, ghostDashed, ghostHidden)

transform
zim DisplayObject method

DESCRIPTION
The transform method adds transform controls to a display object.
The controls allow the user to move, scale, stretch, rotate and change the registration point.
Parameters are available to choose which of these transformations are available.
By default, all the transformations are available to use but
only the scale and registration point controls are showing.
The others work as the user rolls over the edges or the outer corners.
You can optionally set these to be visible as boxes on the sides and circles on the outer corners.

NOTE: works with the ZIM TransformManager() class to handle multiple transforms and saving data for persistence.
NOTE: the transformed object will have its mouseChildren set to false.

CLICK turns off and on the controls if toggle parameter is set to true (default is true)
If you use the TransformManager for multiple objects, the toggle is automatically set to true
SHIFT rotate snaps to 45
Dropping the registration point will snap to corners or center if close enough - unless CTRL is down
CTRL scale will scale about the registration point
CTRL DBLCLICK will reset scale to 1 and rotation to 0

EXAMPLE
rectangle.transform(); // shows handles for tranformations
END EXAMPLE

EXAMPLE
rect.transform({ // scale and stretch only
	move:false,
	rotate:false
});

// hide the rectangle's bottom stretch control so only can stretch from top
// note - transform() expects there to be a control so do not remove a control
// also, the controls have a hitArea so setting alpha to 0 will not work
rect.transformControls.stretchYControls.getChildAt(1).sca(0);
// or set its visible to false
rect.transformControls.stretchYControls.getChildAt(1).visible = false;

// Record the transforms and remake transforms when page reloads
// Or see the TransformManager
if (localStorage && localStorage.data) rect.transformControls.setData(localStorage.data, true);
rect.on("transformed", function() {
	if (localStorage) localStorage.data = rect.transformControls.recordData(true);
});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
move - (default true) let user move object
stretchX - (default true) let user stretch object from left and right sides
stretchY - (default true) let user stretch object from top and bottom
scale - (default true) let user scale object from corners - might still be able to stretch from sides - see stretchX and stretchY
rotate - (default true) let user rotate object
allowToggle - (default true) let user hide and show controls with click - set to false not to let user hide controls
visible - (default true) show the controls to start
onTop - (default true) set to false to not move the selected shape to the top of its container
showStretch - (default false - true on mobile) show side boxes for stretching - a cursor will always show if stretchX or stretchY is true
showRotate - (default false - true on mobile) show circles at corners for rotation - a cursor will always show if rotation is true
showScale - (default true) show corner boxes for scaling - a cursor will always show if scale is set to true
showReg - (default true) show round circle for draggable registration point - rotates around registration point
showBorder - (default true) show rectangle border
borderColor - (default brown) any border color (CSS)
borderWidth - (default 1) the width of the border
dashed - (default false) set to true for dashed border
customCursors - (default true - false on mobile) set to false for system cursors (system cursors will not be rotated)
handleSize - (default 20 mobile - 10 desktop) the size of the control squares and circles
regSize - (default 16) the size of the registration point circle
snapDistance - (default 10) registration point will snap to corners and center if within this distance (and CTRL key not down)
snapRotation - (default 5) rotation will snap to angles divisible by this value
	holding CTRL will avoid snapping
	holding SHIFT will rotate only multiples of 45 degrees
cache (default true) - set to false to not cache the controls and cursors
events (default false) - set to true to receive events while controls are being opertated
	otherwise events will only trigger on pressup (not pressmove) - this conserves processing
ghostColor (default null) - color of outline when transform tools are not showing
ghostWidth (default null) - width of outline when transform tools are not showing
ghostDashed (default true) - dashed of outline if ghostColor or ghostWidth is set
ghostHidden (default false) - set to true to start with ghostHidden

PROPERTIES
toggled - adds a read-only Boolean to the object that is true if controls are showing otherwise false

METHODS
toggle(state - default null) - added to the object
	shows controls if hidden and hides controls if showing (returns the object for chaining)
	or pass in true to show controls or false to hide controls

TRANSFORM CONTROL OBJECT
When tranform() is set on an object, the object receives a transformControls property
This holds the following:

PROPERTIES:
visible - read only whether the controls are visible
ghost - read only as to whether the ghost outline is showing - set with showGhost and hideGhost
ghostEnabled - read only as to whether the ghost outline will be turned on and off - set with addGhost and removeGhost
controls - reference to the Container that holds all the controls
scaleControls - reference to the Container that holds the corner boxes for scaling
stretchXControls - reference to the Container that holds the left and right boxes for stretching
stretchYControls - reference to the Container that holds the top and bottom boxes for stretching
rotateControls - reference to the Container that holds the outer circles for rotating

METHODS:
hide() - hides the controls - returns object for chaining
show() - shows the controls - returns object for chaining
recordData(toJSON) - returns an object with type, x, y, scaleX, scaleY, rotation, skewX, skewY, visible PROPERTIES
	if toJSON (default false) is set to true, the return value is a JSON string
setData(data, fromJSON) - sets the properties to match the data object passed in - this should come from recordData()
	if fromJSON (default false) is set to true, it will assume a JSON string is passed in as data
	returns object for chaining
remove(noHide) - removes the controls - set noHide true if already hidden
add(noShow) - adds the controls back if then have been removed - set noShow true if not wanting to show
allowToggleOn() - sets the show / hide controls on with click
allowToggleOff() - removes the show / hide controls on with click
showGhost() - show the ghost outline - the ghostWidth or ghostColor must be set in initial parameters
hideGhost() - hide the ghost outline
toggleGhost(state) - if ghost is showing will hide ghost and if ghost is hidden will show ghost
	or set state to true to show ghost or false to not show ghost
addGhost() - enable ghost outline functionality - the ghostWidth or ghostColor must be set in initial parameters
removeGhost() - disable ghost outline functionality
disable() - may show the controls if visible but cannot use them
enable() - turns the using of the controls back on
resize(dispatch) - call resize if the object is transformed in ways other than with the controls
	set dispatch to true to dispatch a "transformed" event - if manually adjusted this will save to TransformManager
dispose() - remove all aspects of transform on object

EVENTS
Adds a "transformed" event to obj that is dispatched when pressup on any of the controls or on click
	if the events parameter (or events property on transformControls) is set to true then these happen on pressmove of controls
	the transformed event object has a transformType property
	the transformType property has values of "size", "move", "rotate", "stretch", "reg" "reset"
	the transformed event object also has a pressup property that is true if on pressup and false if from pressmove
Adds "transformshow" and "transformhide" events for when click to hide or show controls
If TransformManager() is used there are more events available such as "persistset", etc.

RETURNS obj for chaining
--*///+33.5
zim.transform = function(obj, move, stretchX, stretchY, scale, rotate, allowToggle, visible, onTop, showStretch, showRotate, showScale, showReg, showBorder, borderColor, borderWidth, dashed, customCursors, handleSize, regSize, snapDistance, snapRotation, cache, events, ghostColor, ghostWidth, ghostDashed, ghostHidden) {
	var sig = "obj, move, stretchX, stretchY, scale, rotate, allowToggle, visible, onTop, showStretch, showRotate, showScale, showReg, showBorder, borderColor, borderWidth, dashed, customCursors, handleSize, regSize, snapDistance, snapRotation, cache, events, ghostColor, ghostWidth, ghostDashed, ghostHidden";
	var duo; if (duo = zob(zim.transform, arguments, sig)) return duo;
	z_d("33.5");

	if (zot(obj) || !obj.getBounds) {zog("zim methods - transform(): please provide object with bounds set"); return obj;}
	if (!obj.getBounds()) {zog("zim methods - transform(): please setBounds() on object");	return obj;}
	if (!obj.parent) {zog("zim methods - transform(): object should be on stage first"); return obj;}

	if (zot(move)) move = true;
	if (zot(stretchX)) stretchX = true;
	if (zot(stretchY)) stretchY = true;
	if (zot(scale)) scale = true;
	if (zot(rotate)) rotate = true;

	var mobile = zim.mobile();

	if (zot(allowToggle)) allowToggle = true;
	if (zot(visible)) visible = true;
	if (zot(onTop)) onTop = true;
	if (zot(showStretch)) showStretch = mobile?true:false;
	if (zot(showRotate)) showRotate = mobile?true:false;
	if (zot(showScale)) showScale = true;
	if (zot(showReg)) showReg = true;
	if (zot(showBorder)) showBorder = true;
	if (zot(borderColor)) borderColor = "brown";
	if (zot(borderWidth)) borderWidth = 1;

	if (zot(ghostColor)) ghostColor = null;
	if (zot(ghostWidth)) ghostWidth = null;
	if (ghostColor < 0 || ghostWidth < 0) ghostColor = ghostWidth = null;
	else if (ghostColor!=null && ghostWidth==null) ghostWidth = 1;
	if (zot(ghostDashed)) ghostDashed = true;
	if (zot(ghostHidden)) ghostHidden = false;


	if (zot(customCursors)) customCursors = mobile?false:true;
	if (zot(handleSize)) handleSize = zim.mobile()?20:10;
	if (zot(regSize)) regSize = 16;
	if (zot(snapDistance)) snapDistance = 10;
	if (zot(snapRotation)) snapRotation = 5;
	if (zot(cache)) cache = true;
	if (zot(events)) events = false;

	obj.mouseChildren = false;
	var con = obj.getConcatenatedMatrix().decompose();

	if (typeof zimDefaultFrame == "undefined") {
		if (typeof frame == "undefined" || frame==null) {
			if (!obj.stage) {
				if (zon) {zog("ZIM transform() - please add object to stage before calling transform()")}
				return obj;
			}
			frame = zimDefaultFrame = {stage:stage, canvas:stage.canvas};
		} else {
			zimDefaultFrame = frame;
		}
	} else if (typeof frame == "undefined" || frame==null) {
		frame = zimDefaultFrame;
	}
	if (zot(obj.stage)) {
		obj.added(function () {
			if (obj.transformControls) {
				obj.transformControls.resize();
			}
		});
	}
	var stage = obj.stage?obj.stage:frame.stage;
	if (zot(frame.eventRemove)) {
		stage.enableMouseOver();
		frame.ctrlKey = false;
		frame.shiftKey = false;
		window.addEventListener("keydown", function(e) {
			frame.altKey = e.altKey;
			frame.ctrlKey = e.ctrlKey;
		});
		window.addEventListener("keyup", function(e) {
			frame.altKey = e.altKey;
			frame.ctrlKey = e.ctrlKey;
		});
	}


	var oB = obj.getBounds();
	var shape = new zim.Shape({style:false}); // bound rect
	var shapeG = new zim.Shape({style:false}); // ghost rect
	var shapeR = new zim.Shape({style:false}); // registration point
	shapeG.mouseEnabled = false;
	shapeR.controlType = "reg";
	var p = obj.parent;
	var g = shape.graphics;
	var gG = shapeG.graphics;
	var gR = shapeR.graphics;
	var pTL;
	var pTR;
	var pBR;
	var pBL;
	var pR;
	var cur;
	var corners;
	var controls = new zim.Container({style:false});
	controls.type = "TransformControls";
	var squares = new zim.Container({style:false});
	var sidesH = new zim.Container({style:false});
	var sidesV = new zim.Container({style:false});
	var rotators = new zim.Container({style:false});
	var totalRotation;
	var totalScaleX;
	var totalScaleY;
	var totalSkewX;
	var totalSkewY;

	// HANDLE MASK if there is one
	if (!zot(obj.zimMaskDynamic)) obj.zimMaskApply(); // set mask set by zimMask to dynamic

	if (customCursors) {
		var moveCursor = new zim.Container({style:false});
		var transformCursor = new zim.Container({style:false});
		makeCursor(moveCursor);
		makeCursor(transformCursor);
		function makeCursor(type) {
			if (type==moveCursor) new zim.Triangle(16,12,12,"white").addTo(type).mov(0,-13.5);
			new zim.Triangle(16,12,12,"white").addTo(type).mov(13.5).rot(90);
			if (type==moveCursor) new zim.Triangle(16,12,12,"white").addTo(type).mov(0,13.5).rot(180);
			new zim.Triangle(16,12,12,"white").addTo(type).mov(-13.5).rot(-90);
			if (type==moveCursor) new zim.Triangle(10,7,7,"black").addTo(type).mov(0,-13);
			new zim.Triangle(10,7,7,"black").addTo(type).mov(13).rot(90);
			if (type==moveCursor) new zim.Triangle(10,7,7,"black").addTo(type).mov(0,13).rot(180);
			new zim.Triangle(10,7,7,"black").addTo(type).mov(-13).rot(-90);
			if (cache) type.cache(-20,-20,40,40);
		}
		var ccData = {"nw-resize":45, "ne-resize":-45, "n-resize":90, "e-resize":0};
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// get total existing transforms
	function getTotals() {
		var temp = obj;
		totalRotation = 0;
		totalScaleX = 1;
		totalScaleY = 1;
		totalSkewX = 0;
		totalSkewY = 0;
		while(temp.parent) {
			totalRotation += temp.rotation;
			totalScaleX *= temp.scaleX;
			totalScaleY *= temp.scaleY;
			totalSkewX += temp.skewX;
			totalSkewY += temp.skewY;
			temp = temp.parent;
		}
	}
	getTotals();
	var originalR = totalRotation;

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// make control shapes

	// corners
	var cursors = ["nw-resize","ne-resize","nw-resize","ne-resize"];
	var opposites = [2,3,0,1];
	for (var i=0; i<4; i++) {
		if (showScale) {
			var rect = new zim.Rectangle(handleSize, handleSize, "#e472c4", "#333", 2, null, null, null, false);
			rect.centerReg(squares);
			if (cache) rect.cache(-1,-1,handleSize+2,handleSize+2);
		} else {
			var rect = new zim.Rectangle(handleSize, handleSize, "rgba(0,0,0,0)", "rgba(0,0,0,0)", 2, null, null, null, false);
			rect.centerReg(squares);
			rect.expand(0);
		}
		rect.rotation = totalRotation;
		cur = customCursors?"none":cursors[i];
		rect.drag({overCursor:cur, dragCursor:cur, onTop:false});
		rect.controlType = "corner";
		rect.cu = cursors[i];
	}
	// run again now that we have all the squares for the opposites
	for (var i=0; i<opposites.length; i++) {
		squares.getChildAt(i).op = squares.getChildAt(opposites[i]);
	}

	// edges
	var cursorsSide = ["n-resize","e-resize","n-resize","e-resize"];
	opposites = [1,1,0,0];
	for (var i=0; i<4; i++) {
		var w = i%2==0?obj.width/2:handleSize;
		var h = i%2==0?handleSize:obj.height/2;
		if (showStretch) {
			var rect = new zim.Rectangle(handleSize, handleSize, "#AAA", "#333", 2, null, null, null, false);
			rect.centerReg(i%2==0?sidesV:sidesH);
			if (cache) rect.cache(-1,-1,handleSize+2,handleSize+2);
		} else {
			var rect = new zim.Rectangle(w, h, "rgba(0,0,0,0)", null, null, null, null, null, false);
			rect.centerReg(i%2==0?sidesV:sidesH);
			rect.expand(0);
		}
		rect.rotation = totalRotation;
		cur = customCursors?"none":cursorsSide[i];
		rect.drag({overCursor:cur, dragCursor:cur, onTop:false});
		rect.controlType = "side";
		rect.cu = cursorsSide[i];
	}
	for (var i=0; i<opposites.length; i++) {
		var whichSide = i%2==0?sidesV:sidesH;
		whichSide.getChildAt(Math.floor(i/2)).op = whichSide.getChildAt(opposites[i]);
	}

	// rotators
	var cursorsRotators = ["ne-resize","nw-resize","ne-resize","nw-resize"];
	for (var i=0; i<4; i++) {

		if (showRotate) {
			var rect = new zim.Circle(handleSize, "#e472c4", null, null, null, null, null, false);
			rect.addTo(rotators);
			if (cache) rect.cache(-handleSize,-handleSize,handleSize*2,handleSize*2);
		} else {
			var rect = new zim.Circle(handleSize, "rgba(0,0,0,0)", null, null, null, null, null, false);
			rect.addTo(rotators);
			rect.expand(0);
		}
		cur = customCursors?"none":cursorsRotators[i];
		rect.drag({overCursor:cur, dragCursor:cur, onTop:false});
		rect.controlType = "rotate";
		rect.cu = cursorsRotators[i];
	}
	var offSet = 1.8;
	rotators.alpha = .5;
	setRotators();
	function setRotators() {
		var shiftRX = handleSize*offSet*zim.sign(totalScaleX);
		var shiftRY = handleSize*offSet*zim.sign(totalScaleY)
		rotators.getChildAt(0).reg(shiftRX, shiftRY);
		rotators.getChildAt(1).reg(-shiftRX, shiftRY);
		rotators.getChildAt(2).reg(-shiftRX, -shiftRY);
		rotators.getChildAt(3).reg(shiftRX, -shiftRY);
		stage.update();
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// draw / redraw controls

	function makeControls() {
		g.c();
		gG.c();
		gR.c();
		pTL = obj.localToGlobal(oB.x, oB.y);
		pTR = obj.localToGlobal(oB.x+oB.width, oB.y);
		pBR = obj.localToGlobal(oB.x+oB.width, oB.y+oB.height);
		pBL = obj.localToGlobal(oB.x, oB.y+oB.height);

		pTM = obj.localToGlobal(oB.x+oB.width/2, oB.y);
		pRM = obj.localToGlobal(oB.x+oB.width, oB.y+oB.height/2);
		pBM = obj.localToGlobal(oB.x+oB.width/2, oB.y+oB.height);
		pLM = obj.localToGlobal(oB.x, oB.y+oB.height/2);

		pR = obj.localToGlobal(obj.regX, obj.regY);
		pMid = obj.localToGlobal(oB.x+oB.width/2, oB.y+oB.height/2);
		corners = [pTL, pTR, pBR, pBL];
		mids = [pTM, pRM, pBM, pLM];

		if (showBorder) {
			g.s(borderColor);
			if (dashed) g.sd([10, 5], 0);
			g.ss(borderWidth)
				.mt(pTL.x, pTL.y)
				.lt(pTR.x, pTR.y)
				.lt(pBR.x, pBR.y)
				.lt(pBL.x, pBL.y)
				.lt(pTL.x, pTL.y)
				.cp();
		}
		if (!zot(ghostColor)) {
			gG.s(ghostColor);
			if (ghostDashed) gG.sd([10, 5], 0);
			gG.ss(ghostWidth)
				.mt(pTL.x, pTL.y)
				.lt(pTR.x, pTR.y)
				.lt(pBR.x, pBR.y)
				.lt(pBL.x, pBL.y)
				.lt(pTL.x, pTL.y)
				.cp();
		}
		shape.setBounds(pTL.x, pTL.y, pTR.x-pTL.x, pBL.y-pTL.y);

		// circle at registration point
		if (showReg) {
			gR.s("#eee").ss(borderWidth).f("rgba(0,0,0,.1)").dc(0,0,regSize);
			gR.s("#222").ss(borderWidth).dc(0,0,regSize*.3);
		}

		getTotals(); // rotate, scales, etc.

		var skX = Math.min(50,Math.abs(totalSkewX))*zim.sign(totalSkewX);
		var skY = Math.min(50,Math.abs(totalSkewY))*zim.sign(totalSkewY);
		var ro = (totalRotation)*zim.sign(totalScaleX*totalScaleY)*zim.sign(obj.scaleX*obj.scaleY);

		for (var i=0; i<corners.length; i++) {
			var c = corners[i];
			squares.getChildAt(i)
				.loc(c.x, c.y)
				.ske(skX, skY)
				.rot(ro);
			rotators.getChildAt(i)
				.loc(c.x, c.y)
				.rot(ro);
			var m = mids[i];
			var whichSide = i%2==0?sidesV:sidesH;
			whichSide.getChildAt(Math.floor(i/2))
				.loc(m.x, m.y)
				.ske(skX, skY)
				.rot(ro);
			if (!showStretch) {
				whichSide.getChildAt(Math.floor(i/2)).sca(i%2==0?totalScaleX/2:1, i%2==0?1:totalScaleY/2);
			}
		}
		shapeR.x = pR.x;
		shapeR.y = pR.y;
	}
	makeControls();

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// dragger
	var dragger = new zim.Shape(1000,1000,null,null,null,false);
	function drawDragger() {
		dragger.x = 0;
		dragger.y = 0;
		dragger.graphics
			.c()
			.f("rgba(0,0,0,.01)")
			.mt(pTL.x, pTL.y)
			.lt(pTR.x, pTR.y)
			.lt(pBR.x, pBR.y)
			.lt(pBL.x, pBL.y)
			.lt(pTL.x, pTL.y)
			.cp();
		dragger.reg(pR.x, pR.y)
		dragger.x = pR.x;
		dragger.y = pR.y;
	}
	drawDragger();

	controls.on("dblclick", function(e) {
		if (frame.ctrlKey && obj.transformControls.visible) {
			obj.scaleX = 1;
			obj.scaleY = 1;
			obj.rotation = 0;
			// if (customCursors) moveCursor.rot(obj.rotation);
			makeControls();
			drawDragger();
			stage.update();
		}
		pressUp(e, true);
	});

	var toggleControlsEvent = controls.on("mousedown", toggleCheck);
	var toggleObjEvent = obj.on("mousedown", toggleCheck);
	function toggleCheck(e) {
		var underPoint = stage.getObjectUnderPoint(e.stageX, e.stageY, 1);
		// if (obj.hitTestPoint(e.stageX, e.stageY)) {
		if (zim.mobile() || (underPoint && ((obj.contains && obj.contains(underPoint)) || (!obj.contains && obj == underPoint)))) {
			if (!obj.transformControls.visible) {
				if (customCursors) {
					dragReady = false; // wait until the first pressup to show custom cursor
					dragger.mouseEnabled = false;
				} else {
					frame.canvas.style.cursor = "move";
				}
				obj.transformControls.show();
				obj.dispatchEvent("transformshow");
			}
		} else {
			if (obj.transformControls.visible && (!underPoint || !controls.contains(underPoint))) {
				obj.transformControls.hide();
				obj.dispatchEvent("transformhide");
			}
		}
	}
	var toggleStageEvent = stage.on("stagemousedown", function(e) {
		var underPoint = stage.getObjectUnderPoint(e.stageX, e.stageY, 1);
		var layerPress = (underPoint && underPoint.parent && underPoint.parent.layer && underPoint.parent.layer == obj);

		// if (obj.transformControls.visible && !controls.hitTestPoint(e.stageX, e.stageY)) {
		if (obj.transformControls.visible && (!underPoint || (!controls.contains(underPoint) && !layerPress))) {
			obj.dispatchEvent("transformhide");
			obj.transformControls.hide();
		}
	});
	if (!allowToggle) {
		controls.off("mousedown", toggleControlsEvent);
		obj.off("mousedown", toggleObjEvent);
		stage.off("stagemousedown", toggleStageEvent);
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// scaling
	var startX; // target start x
	var startY; // target start y
	var startR; // rotation of object to start
	var startAngle; // angle of mouse down to x axis
	var diffX;
	var diffY;
	var cornerPoint;
	var objStartX; // start object x in parent frame
	var objStartY; // start object x in parent frame
	var objRX; // start object x in global frame
	var objRY; // start object y in global frame
	var rotateCheck = false;
	var mousePress = false;
	var mousemoveEvent;
	var dragmoveEvent;
	var objCursor = obj.cursor;
	var transformEvent;
	var carrier = new zim.Circle(30, "rgba(0,0,0,0)", null, null, null, null, null, false).expand(20);
	if (customCursors) {
		carrier.mouseEnabled = false;
		carrier.mouseChildren = false;

		// these should be removed when transform is hidden!
		carrier.addChild(transformCursor);
		squares.on("mouseover", showCustomCursor);
		squares.on("mouseout", hideCustomCursor);
		sidesH.on("mouseover", showCustomCursor);
		sidesH.on("mouseout", hideCustomCursor);
		sidesV.on("mouseover", showCustomCursor);
		sidesV.on("mouseout", hideCustomCursor);
		rotators.on("mouseover", showCustomCursor);
		rotators.on("mouseout", hideCustomCursor);
		var carrier2 = new zim.Circle(30, "rgba(0,0,0,0)", null, null, null, null, null, false).expand(20);
		carrier2.cursor = "none";
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// scale corners
	squares.on("mousedown", transformMousedown);
	squares.on("pressmove", scalePressmove);
	squares.on("pressup", pressUp);

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// scale sides
	sidesH.on("mousedown", transformMousedown);
	sidesH.on("pressmove", scalePressmove);
	sidesH.on("pressup", pressUp);
	sidesV.on("mousedown", transformMousedown);
	sidesV.on("pressmove", scalePressmove);
	sidesV.on("pressup", pressUp);

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// rotate
	rotators.on("mousedown", transformMousedown);
	rotators.on("pressmove", rotatePressmove);
	rotators.on("pressup", pressUp);

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// transform functions

	function transformMousedown(e) {
		mousePress = true;
		if (onTop) {
			upTop();
			// obj.parent.setChildIndex(obj, obj.parent.numChildren-1);
			// stage.addChild(controls);
		}
		if (mousemoveEvent) stage.off("stagemousemove", mousemoveEvent);
		startX = e.target.x;
		startY = e.target.y;
		objStartX = obj.x;
		objStartY = obj.y;
		startSX = obj.scaleX;
		startSY = obj.scaleY;
		if (e.target.controlType == "rotate") {
			rotateCheck = true;
			startR = obj.rotation;
			var startRX = e.stageX/stage.scaleX;
			var startRY = e.stageY/stage.scaleY;
			var point = p.localToGlobal(objStartX, objStartY);
			objRX = point.x;
			objRY = point.y;
			startAngle = Math.atan2(startRY-objRY, startRX-objRX)*180/Math.PI;
		}
		if (frame.ctrlKey || e.target.controlType == "rotate") { // ctrl scale and rotation happen around
			diffX = Math.abs(startX-obj.x);
			diffY = Math.abs(startY-obj.y);
		} else {
			diffX = Math.abs(startX-e.target.op.x);
			diffY = Math.abs(startY-e.target.op.y);
		}
		cornerPointObj = e.target.op;

		if (e.target.controlType != "rotate") cornerPoint = e.target.op.parent.localToLocal(e.target.op.x, e.target.op.y, obj.parent); // cornerPoint = {x:e.target.op.x, y:e.target.op.y};
		if (carrier.stage) stage.setChildIndex(carrier, stage.getChildIndex(controls));
		else carrier.addTo(stage, stage.getChildIndex(controls)+1);
		carrier.x = e.stageX/stage.scaleX;
		carrier.y = e.stageY/stage.scaleY;
		carrier.cursor = customCursors?"none":e.target.cu;
		if (customCursors) {
			obj.getConcatenatedMatrix().decompose(con)
			if (!con.rotation) con.rotation = 0;
			transformCursor.rotation = con.rotation*zim.sign(totalScaleX*totalScaleY)*zim.sign(obj.scaleX*obj.scaleY) + ccData[e.target.cu] + originalR;
			if (e.target.controlType != "side" && totalScaleX * totalScaleY < 0) transformCursor.rotation += 90;
			carrier2.addTo(stage, 1).pos({x:e.stageX/stage.scaleX, y:e.stageY/stage.scaleY, reg:true});
		}
		dragger.visible = false;
		obj.cursor = "none";
	};

	function scalePressmove(e) {
		var scale;
		if (frame.ctrlKey && e.target.controlType == "corner") { // relative to registration point
			scale = (diffX > diffY) ? (e.target.x - objStartX) / (startX-objStartX) : (e.target.y - objStartY) / (startY-objStartY);
		} else { // relative to opposite corner
			scale = (diffX > diffY) ? (e.target.x - e.target.op.x) / (startX-e.target.op.x) : (e.target.y - e.target.op.y) / (startY-e.target.op.y);
		}
		if (e.target.controlType == "corner") {
			obj.scaleX = scale * startSX;
			obj.scaleY = scale * startSY;
		} else {
			obj.scaleX = (e.target.cu == "e-resize") ? scale * startSX : startSX;
			obj.scaleY = (e.target.cu == "n-resize") ? scale * startSY : startSY;
		}
		makeControls();
		carrier.x = e.stageX/stage.scaleX;
		carrier.y = e.stageY/stage.scaleY;
		if (customCursors) {carrier2.x = e.stageX/stage.scaleX; carrier2.y = e.stageY/stage.scaleY;}
		if (!frame.ctrlKey || e.target.controlType == "side") { // keep opposite corner at same location
			var newCornerPoint = e.target.op.parent.localToLocal(e.target.op.x, e.target.op.y, obj.parent);
			obj.x -= (newCornerPoint.x-cornerPoint.x) * zim.sign(totalScaleX) * zim.sign(obj.scaleX); // adjust for - scale outside
			obj.y -= (newCornerPoint.y-cornerPoint.y) * zim.sign(totalScaleY) * zim.sign(obj.scaleY);
		}
		makeControls();
		if (obj.transformControls.events) {
			var type = e.target.controlType;
			dispatchEvents(e, type);
		}
	};

	function rotatePressmove(e) {
		var angle = Math.atan2(e.stageY/stage.scaleY-objRY, e.stageX/stage.scaleX-objRX)*180/Math.PI;
		if (frame.shiftKey) {
			obj.rot(Math.round((startR + (angle - startAngle) * zim.sign(totalScaleX) * zim.sign(obj.scaleX))/45)*45);
		} else {
			obj.rot(startR + (angle - startAngle) * zim.sign(totalScaleX) * zim.sign(obj.scaleX));
		}
		makeControls();
		carrier.x = e.stageX/stage.scaleX;
		carrier.y = e.stageY/stage.scaleY;
		if (customCursors) {
			obj.getConcatenatedMatrix().decompose(con)
			if (!con.rotation) con.rotation = 0;
			transformCursor.rotation = con.rotation*zim.sign(totalScaleX*totalScaleY)*zim.sign(obj.scaleX*obj.scaleY) + ccData[e.target.cu] + originalR;
			if (e.target.controlType != "side" && totalScaleX * totalScaleY < 0) transformCursor.rotation += 90;
			carrier2.x = e.stageX/stage.scaleX;
			carrier2.y = e.stageY/stage.scaleY;
		}
		makeControls();
		if (obj.transformControls.events) {
			var type = e.target.controlType;
			dispatchEvents(e, type, null, false);
		}
	};

	function pressUp(e, reset) {
		setRotators();
		var type = e ? e.target.controlType : "move";
		if (reset) type = "reset";
		if (customCursors) carrier2.removeFrom(stage);
		dispatchEvents(e, type, true);
		stage.update();
	}

	function dispatchEvents(e, type, pressup, cursors) {
		if (cursors==null) cursors = true;
		var tCheck = false;
		transformEvent = null;
		if (type == "move") {
			if (obj.x != startProperties.x || obj.y != startProperties.y) {
				transformEvent = new createjs.Event("transformed");
				transformEvent.transformType = "move";
			}
			if (cursors) tCheck = dragger.hitTestPoint(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
		} else if (type == "corner") {
			if (obj.scaleX != startProperties.scaleX || obj.scaleY != startProperties.scaleY) {
				transformEvent = new createjs.Event("transformed");
				transformEvent.transformType = "size";
			}
			if (cursors) tCheck = squares.hitTestPoint(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
		} else if (type == "side") {
			if (obj.scaleX != startProperties.scaleX || obj.scaleY != startProperties.scaleY) {
				transformEvent = new createjs.Event("transformed");
				transformEvent.transformType = "stretch";
			}
			if (cursors) tCheck = sidesH.hitTestPoint(e.stageX/stage.scaleX, e.stageY/stage.scaleY) || sidesV.hitTestPoint(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
		} else if (type == "rotate") {
			if (obj.rotation != startProperties.rotation) {
				transformEvent = new createjs.Event("transformed");
				transformEvent.transformType = "rotate";
			}
			if (cursors) tCheck = rotators.hitTestPoint(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
		} else if (type == "reg") {
			if (obj.regX != startProperties.regX || obj.regY != startProperties.regY) {
				transformEvent = new createjs.Event("transformed");
				transformEvent.transformType = "reg";
			}
		} else if (type == "reset") {
			transformEvent = new createjs.Event("transformed");
			transformEvent.transformType = "reset";
		} else {
			transformEvent = new createjs.Event("transformed");
			transformEvent.transformType = "unknown";
		}
		if (cursors && !tCheck) {
			carrier.removeFrom(stage);
			frame.canvas.style.cursor = "default";
			obj.cursor = objCursor;
		}
		// stage.update();
		if (transformEvent) {
			transformEvent.pressup = pressup;
			obj.dispatchEvent(transformEvent);
		}
	}

	function showCustomCursor(e) { // not drag
		if (mousePress) return;
		if (carrier.stage) stage.setChildIndex(carrier, stage.getChildIndex(controls));
		else carrier.addTo(stage, stage.getChildIndex(controls)+1);
		carrier2.addTo(stage, 1);
		carrier.x = e.stageX/stage.scaleX;
		carrier.y = e.stageY/stage.scaleY;
		carrier2.x = e.stageX/stage.scaleX;
		carrier2.y = e.stageY/stage.scaleY;
		obj.getConcatenatedMatrix().decompose(con)
		if (!con.rotation) con.rotation = 0;
		transformCursor.rotation = con.rotation*zim.sign(totalScaleX*totalScaleY)*zim.sign(obj.scaleX*obj.scaleY) + ccData[e.target.cu] + originalR;
		if (e.target.controlType != "side" && totalScaleX * totalScaleY < 0) transformCursor.rotation += 90;
		stage.update();
		if (mousemoveEvent) stage.off("stagemousemove", mousemoveEvent);
		mousemoveEvent = stage.on("stagemousemove", function(e) {
			carrier.x = e.stageX/stage.scaleX;
			carrier.y = e.stageY/stage.scaleY;
			stage.update();
		});
	}

	function hideCustomCursor(e) { // not drag
		stage.off("stagemousemove", mousemoveEvent);
		if (mousePress) return;
		obj.cursor = objCursor;
		carrier.removeFrom(stage);
		carrier2.removeFrom(stage);
		stage.update();
	}

	function upTop() {
		if (onTop) {
			var nc = p.numChildren-1;
			if (p.getChildAt(nc).type == "Keyboard") nc--;
			p.setChildIndex(obj, nc);
			addControls();
		}
	}

	function addControls() {
		// if the container is the stage add the controls one above the object
		// else add the controls one above the container
		if (p==stage) {
			controls.removeFrom();
			stage.addChildAt(controls, stage.getChildIndex(obj)+1);
		} else {
			var insert = p;
			while (insert.parent && insert.parent!=stage) {insert = insert.parent;}
			if (obj.stage) {
				if (controls.parent==stage) stage.removeChild(controls);
				stage.addChildAt(controls, stage.getChildIndex(insert)+1);
			}
		}
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// drag registration point

	shapeR.drag();
	shapeR.on("mousedown", upTop);
	shapeR.on("pressup", function(e) {
		// snap to corners unless the ctrl key is down
		if (!frame.ctrlKey) {
			for (var i=0; i<corners.length; i++) {
				if (zim.dist(shapeR.x, shapeR.y, corners[i].x, corners[i].y) < snapDistance) {
					shapeR.x = corners[i].x;
					shapeR.y = corners[i].y;
					break;
				}
			}
			if (zim.dist(shapeR.x, shapeR.y, pMid.x, pMid.y) < snapDistance) {
				shapeR.x = pMid.x;
				shapeR.y = pMid.y;
			}
		}
		var originalRotation = obj.rotation;
		var point = obj.globalToLocal(shapeR.x, shapeR.y);
		if (p==stage) var rPoint = new zim.Point(shapeR.x, shapeR.y);
		else var rPoint = p.globalToLocal(shapeR.x, shapeR.y);
		obj.reg(point.x, point.y);
		obj.rotation = 0;
		obj.x = obj.x + rPoint.x-obj.x;
		obj.y = obj.y + rPoint.y-obj.y;
		obj.rotation = originalRotation;

		makeControls();
		drawDragger();
		pressUp(e);
		stage.update()
	});

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	//  dragger functions
	var dragReady = true;
	cur = customCursors ? "none" : "move";
	dragger.drag({overCursor:cur, dragCursor:cur, onTop:false});
	dragger.controlType = "move";
	if (customCursors) {
		dragger.on("mouseover", draggerOver);
		dragger.on("mouseout", draggerOut);
	}
	function draggerOut() {
		stage.off("stagemousemove", dragmoveEvent);
		if (mousePress) return;
		moveCursor.removeFrom(controls);
		stage.update();
	};
	function draggerOver(e) {
		if (mousePress || !customCursors || !dragReady) return;
		moveCursor
			.addTo(controls)
			// .rot(obj.rotation*zim.sign(totalScaleX*totalScaleY)*zim.sign(obj.scaleX*obj.scaleY))
			.pos({x:e.stageX/stage.scaleX, y:e.stageY/stage.scaleY, reg:true});
		stage.update();
		if (dragmoveEvent) stage.off("stagemousemove", dragmoveEvent);
		dragmoveEvent = stage.on("stagemousemove", function(e) {
			moveCursor.x = e.stageX/stage.scaleX;
			moveCursor.y = e.stageY/stage.scaleY;
			stage.update();
		});
	}
	dragger.on("mousedown", upTop);
	dragger.on("pressmove", function(e) {
		if (!obj.transformControls.visible) return;
		if (p==stage) var point = new zim.Point(dragger.x, dragger.y);
		else var point = p.globalToLocal(dragger.x, dragger.y);
		obj.x = point.x;
		obj.y = point.y;
		makeControls();
		if (obj.transformControls.events) {
			var type = "move";
			dispatchEvents(e, type);
		}
	});
	dragger.on("pressup", function(e){
		if (!obj.transformControls.visible) return;
		pressUp(e);
	});

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// Stage Mouse Up
	var stageUpEvent = stage.on("stagemouseup", function() {
		mousePress = false;
		dragger.mouseEnabled = true;
		if (!dragReady) {
			if (obj.x != startProperties.x || obj.y != startProperties.y) {
				transformEvent = new createjs.Event("transformed");
				transformEvent.transformType = "move";
				obj.dispatchEvent(transformEvent);
			}
		}
		dragReady = true;
		dragger.visible = true;
		if (rotateCheck) {
			if (!frame.ctrlKey && snapRotation > 1) { // snap if not control
				obj.rotation = Math.round(obj.rotation/snapRotation)*snapRotation;
				makeControls();
			}
		}
		drawDragger();
		dragger.mouseEnabled = true;
		rotateCheck = false;
		stage.update();
	});

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// add controls to controls container

	function getStartProperies() {
		startProperties = {x:obj.x, y:obj.y, rotation:obj.rotation, regX:obj.regX, regY:obj.regY, scaleX:obj.scaleX, scaleY:obj.scaleY};
	}
	var startProperties;
	getStartProperies();
	var compareObjEvent = obj.on("mousedown", getStartProperies);
	var compareControlEvent = controls.on("mousedown", getStartProperies);

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// add controls to controls container

	controls.addChild(shape);
	if (move) controls.addChild(dragger);
	if (scale) controls.addChild(squares);
	if (stretchX) controls.addChild(sidesH);
	if (stretchY) controls.addChild(sidesV);
	if (rotate) controls.addChild(rotators);
	controls.addChild(shapeR);

	var pressmoveEvent;
	if (move) {
		obj.drag({overCursor:"pointer", dragCursor:"pointer", onTop:false, removeTweens:false});
		pressmoveEvent = obj.on("pressmove", function() {
			makeControls();
			drawDragger();
			setRotators();
		});
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// control object

	var toggleStart = allowToggle;
	obj.transformControls = {
		visible:visible,
		events:events,
		ghost:!zot(ghostColor),
		ghostEnabled:!zot(ghostColor),
		show:function() {
			if (onTop) upTop();
			// else addControls // taken out in 9.5.0 and made always adding - removed add from upTop
			addControls();
			obj.toggled = true;
			obj.transformControls.visible = true;
			if (shapeG.parent) shapeG.removeFrom();
			stage.update();
			return obj;
		},
		hide:function() {
			if (obj.transformControls.ghost) var index = stage.getChildIndex(controls);
			stage.removeChild(controls);
			if (obj.transformControls.ghost && obj.transformControls.ghostEnabled) stage.addChildAt(shapeG, index);
			obj.toggled = false;
			obj.transformControls.visible = false;
			if (customCursors) {
				if (mousemoveEvent) stage.off("stagemousemove", mousemoveEvent);
				if (dragmoveEvent) stage.off("stagemousemove", dragmoveEvent);
				mousePress = false;
				carrier.removeFrom(stage);
				carrier2.removeFrom(stage);
			}
			if (!move) obj.cursor = objCursor;
			stage.update();
			return obj;
		},
		remove:function(noHide) {
			if (!noHide) {
				obj.transformControls.hide();
			}
			toggle = false;
			if (move) {
				obj.noDrag();
				obj.off("pressmove", pressmoveEvent);
			}
		},
		add:function(noShow) {
			if (!noShow) {
				obj.transformControls.show();
			}
			toggle = toggleStart;
			if (move) {
				obj.drag({overCursor:"pointer", dragCursor:"pointer", onTop:false, removeTweens:false});
				pressmoveEvent = obj.on("pressmove", pressmoveEvent);
			}
		},
		allowToggleOn:function() {
			toggle = toggleStart = true;
			toggleControlsEvent = controls.on("mousedown", toggleControlsEvent);
			toggleObjEvent = obj.on("mousedown", toggleObjEvent);
			toggleStageEvent = stage.on("stagemousedown", toggleStageEvent);
		},
		allowToggleOff:function() {
			toggle = toggleStart = false;
			controls.off("mousedown", toggleControlsEvent);
			obj.off("mousedown", toggleObjEvent);
			stage.off("stagemousedown", toggleStageEvent);
		},
		disable:function() {
			controls.mouseChildren = false;
			controls.mouseEnabled = false;
			if (move) {
				obj.noDrag();
				obj.off("pressmove", pressmoveEvent);
			}
		},
		enable:function() {
			controls.mouseChildren = true;
			controls.mouseEnabled = true;
			if (move) {
				obj.drag({overCursor:"pointer", dragCursor:"pointer", onTop:false, removeTweens:false});
				pressmoveEvent = obj.on("pressmove", pressmoveEvent);
			}
		},
		recordData:function(toJSON) {
			var data = { // ES6 would help...
				type:obj.type,
				index:p.getChildIndex(obj),
				x:obj.x, y:obj.y,
				scaleX:obj.scaleX,
				scaleY:obj.scaleY,
				rotation:obj.rotation,
				skewX:obj.skewX,
				skewY:obj.skewY,
				regX:obj.regX,
				regY:obj.regY,
				controls:obj.transformControls.visible
			}
			return toJSON ? JSON.stringify(data) : data;
		},
		setData:function(data, fromJSON) {
			if (zot(data)) return;
			if (fromJSON) data = JSON.parse(data);
			var visible = data.controls;
			delete data.controls;
			var index = data.index;
			if (zot(index)) index = 0;
			delete data.index;
			for (var i in data) {
				obj[i] = data[i];
			}
			timeout(90, function () { // added 9.5.0 - watch to see if there is an issue - nested positions were not ready
				makeControls();
				drawDragger();
				setRotators();
				p.setChildIndex(obj, index);
				if (obj.transformControls.visible && obj.type != "Layer") obj.transformControls.show();
				else obj.transformControls.hide();
				if (obj.transformControls.ghost && !obj.transformControls.ghostEnabled && !obj.transformControls.visible) stage.addChild(shapeG);
			})
			return obj;
		},
		hideGhost:function() {
			obj.transformControls.ghost = false;
			shapeG.removeFrom();
        },
        showGhost:function() {
			obj.transformControls.ghost = true;
			if (obj.transformControls.ghostEnabled && !obj.transformControls.visible) {
				var insert = p;
				while (insert.parent && insert.parent!=stage) {insert = insert.parent;}
				if (shapeG.stage) {
					stage.setChildIndex(shapeG, stage.getChildIndex(insert)+1);
				} else {
					stage.addChildAt(shapeG, stage.getChildIndex(insert)+1);
				}
			}
        },
		addGhost:function() {
			if (ghostWidth > 0) {
				obj.transformControls.ghostEnabled = true;
				obj.transformControls.showGhost();
			}
			else {if (zon) zog("ZIM transform() - ghostWidth must be > 0 when making transform");}
        },
        removeGhost:function() {
			obj.transformControls.ghostEnabled = false;
			obj.transformControls.hideGhost();
        },
		resize:function(dispatch) {
			p = obj.parent;
			makeControls();
			drawDragger();
			setRotators();
			if (visible) obj.transformControls.show(); // bring on top
			if (dispatch) {
				var transformEvent = new createjs.Event("transformed");
				transformEvent.pressup = true;
				transformEvent.transformType = "resize";
				obj.dispatchEvent(transformEvent);
			}
			return obj;
		},
		dispose:function() {
			obj.transformControls.hide();
			toggle = false;
			if (move) {
				obj.noDrag();
				obj.off("pressmove", pressmoveEvent);
			}
			controls.removeAllEventListeners();
			if (toggleObjEvent) obj.off("mousedown", toggleObjEvent);
			if (toggleControlsEvent) obj.off("mousedown", toggleControlsEvent);
			if (toggleStageEvent) stage.off("stagemousedown", toggleStageEvent);
			if (mousemoveEvent) stage.off("stagemousemove", mousemoveEvent);
			if (dragmoveEvent) stage.off("stagemousemove", dragmoveEvent);
			if (stageUpEvent) stage.off("stagemouseup", stageUpEvent);
			if (compareObjEvent) obj.off("mousedown", compareObjEvent);
			if (pressmoveEvent) obj.off("pressmove", pressmoveEvent);
			controls = carrier = carrier2 = null;
			obj.transformControls = null;
		},
		controls:controls,
		scaleControls:squares,
		stretchXControls:sidesH,
		stretchYControls:sidesV,
		rotateControls:rotators
	}

	obj.toggle = function(state) {
		if (state===true) obj.transformControls.show();
		else if (state===false) obj.transformControls.hide();
		else if (obj.transformControls.visible) obj.transformControls.hide();
		else obj.transformControls.show();
		return obj;
	}
	if (obj.transformControls.ghost) {
		obj.transformControls.toggleGhost = function(state) {
			if (state===true) obj.transformControls.showGhost();
			else if (state===false) obj.transformControls.hideGhost();
			else if (obj.transformControls.visible) obj.transformControls.hideGhost();
			else obj.transformControls.showGhost();
			return obj;
		}
	}
	if (visible) obj.transformControls.show();
	else if (!ghostHidden && obj.transformControls.ghost) obj.transformControls.showGhost();

	return obj;

}//-33.5


/*--
obj.gesture = function(move, scale, rotate, boundary, minScale, maxScale, snapRotate, localBounds, slide, slideEffect, regControl, onTop, surround, circularBounds, rect)

gesture
zim DisplayObject method

DESCRIPTION
Sets multi-touch pan, pinch and rotate for position, scale and rotation
Handles scaled and rotated containers
Scale and rotation occur from the pinch point (with optional regControl for about the registration point)
Note - gesture() only works on the currentTarget - not a container's children (like drag() can)
ZIM Frame should have touch set to true (which is the default for mobile)
ALSO: see the noGesture() method to remove some or all gestures
ALSO: see the gestureBoundary() method to set or reset the boundary rectangle dynamically

EXAMPLE
rectangle.gesture(); // move, scale and rotate with no bounds
END EXAMPLE

EXAMPLE
rect.gesture({
	rotate:false,
	rect:new createjs.Rectangle(0,0,stageW,stageH),
	minScale:.5,
	maxScale:3,
	slide:true
});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
move - (default true) move the object with average of fingers
scale - (default true) scale the object with first two fingers' pinch
rotate - (default true) rotate the object with first two fingers' rotation
boundary - (default null) ZIM Boundary rectangle with (x,y,w,h) to contain bounds of the object
	if surround is true then it will make sure the obj surrounds the boundary rather than stays within it
	See circularBounds parameter to keep circular shapes with boundary
minScale - (default null) a minimum scale
maxScale - (default null) a maximum scale
snapRotate - (default 1) degrees to snap rotation to after rotation is finished
localBounds - (default false) set to true to make rect for bounds local rather than global
slide - (default false) will let you throw the object and dispatch a slidestop event when done
slideEffect - (default 5) how much slide with 0 being no slide and then longer slide times and distance like 10, etc.
regControl (default false) set to true to rotate and scale around registration point rather than pinch point
onTop - (default true) brings the object to the top of the container
surround - (default false) is for dragging a big object that always surrounds the rect
	must specify a rect and currently not supported unless rotate is false
circularBounds - (default false) set to true if object is circular with center registration
	set to true to use radius to calculate rotated object in boundary if boundary is set
rect - (depreciated) same as boundary - kept for backwards compatibility

EVENTS
Adds move, scale and rotate events to obj (when associated gesture parameters are set to true)
If slide is true, obj dispatches a "slidestop" event when sliding stops

RETURNS obj for chaining
--*///+34.5
	zim.gesture = function(obj, move, scale, rotate, boundary, minScale, maxScale, snapRotate, localBounds, slide, slideEffect, regControl, onTop, surround, circularBounds, rect) {
		var sig = "obj, move, scale, rotate, boundary, minScale, maxScale, snapRotate, localBounds, slide, slideEffect, regControl, onTop, surround, circularBounds, rect";
		var duo; if (duo = zob(zim.gesture, arguments, sig)) return duo;
		z_d("34.5");

		if (zot(obj) || !obj.on) return;
		if (zot(move)) move = true;
		if (zot(scale)) scale = true;
		if (zot(rotate)) rotate = true;
		if (zot(localBounds)) localBounds = false;
		if (zot(snapRotate)) snapRotate = 1;
		if (zot(slide)) slide = false;
		if (zot(slideEffect)) slideEffect = 5;
		if (zot(regControl)) regControl = false;
		if (zot(onTop)) onTop = true;
		if (zot(surround)) surround = false;
		if (surround && rotate) {
			surround = rect = null;
			if (zon) zog("ZIM Gesture() - does not support surround when rotate is true");
		}
		if (zot(circularBounds)) circularBounds = false;
		if (zot(boundary) && !zot(rect)) boundary = rect;

		var slideData;
		var slideCount;

		// HANDLE MASK if there is one
		if (!zot(obj.zimMaskDynamic)) obj.zimMaskApply(); // set mask set by zimMask to dynamic

		if (!obj.zimTouch) {

			var dampScaleX = new zim.Damp(obj.scaleX, .05);
			var dampScaleY = new zim.Damp(obj.scaleY, .05);
			var scaleRatio = obj.scaleX/obj.scaleY;

			obj.zimTouch = {
				move:move, // store settings on object to control with noGesture()
				scale:scale,
				rotate:rotate,
				pointers:{}, // holds the current pointer data
				checkBounds:function(x, y) { // used locally and by zim.gestureBoundary
					if (obj.zimTouch.boundary) {
						var boundary = obj.zimTouch.boundary;
						// convert the desired drag position to a global point
						// note that we want the position of the object in its parent
						// so we use the parent as the local frame
						var point = obj.parent.localToLocal(x,y,obj.parent);
						// boundary is the boundary rectangle on the global stage
						// boundary is set during mousedown to allow for global scaling when in localBounds mode
						// if you scale in localBounds==false mode, you will need to reset bounds with dragBoundary()
						if (surround) {
							x = Math.min(boundary.x, Math.max(boundary.x+boundary.width, point.x));
							y = Math.min(boundary.y, Math.max(boundary.y+boundary.height, point.y));
						} else {
							x = Math.max(boundary.x, Math.min(boundary.x+boundary.width, point.x));
							y = Math.max(boundary.y, Math.min(boundary.y+boundary.height, point.y));
						}
						// now that the point has been checked on the global scale
						// convert the point back to the obj parent frame of reference
						point = obj.parent.globalToLocal(x, y);
						x = point.x;
						y = point.y;
					}
					return {x:x,y:y}
				}
			};

			if (boundary) {
				obj.zimTouch.boundary = boundary;
				if (localBounds) obj.zimTouch.boundary = zim.boundsToGlobal(obj.parent, boundary);
				obj.zimTouch.boundaryStartX = obj.zimTouch.boundary.x;
				obj.zimTouch.boundaryStartY = obj.zimTouch.boundary.y;
				obj.zimTouch.boundaryStartW = obj.zimTouch.boundary.width;
				obj.zimTouch.boundaryStartH = obj.zimTouch.boundary.height;
				var result = obj.zimTouch.checkBounds(obj.x, obj.y); // set in bounds to start
				obj.x = result.x;
				obj.y = result.y;
			}

			if (slide) {
				slideSlice = 10;
				slideTotal = 5;
				slideCount = 0;
				slideData = [];
				obj.zimTouch.slideInterval = zim.interval(slideSlice,function() {
					slideData[slideCount++%slideTotal] = [obj.x, obj.y];
				});
				obj.zimTouch.slideInterval.pause();
				obj.animate({x:obj.x, y:obj.y}, 10, "quadOut"); // for some reason, first throw is smoother if already animated
			}

			var matrixStart;
			var startScaleX;
			var startScaleY;
			var startRotation;
			var startRegX;
			var startRegY;
			var lastPoint;
			var maxTouches;

			obj.zimTouch.mousedown = obj.on("mousedown", function(e) {

				if (zot(maxTouches)) maxTouches = 1;
				else maxTouches++;

				var stage = obj.stage;
				if (onTop) {
					var nc = obj.parent.numChildren-1;
					if (obj.parent.getChildAt(nc).type=="Keyboard") nc--;
					obj.parent.setChildIndex(obj,nc);
					if (obj.ZIMoutlineShape) obj.outline();
				}
				if (!regControl) {
					lastPoint = null;
					startScaleX = obj.scaleX;
					startScaleY = obj.scaleY;
					startRotation = obj.rotation;
					startRegX = obj.regX;
					startRegY = obj.regY;
					obj.reg(0,0);
					matrixStart = obj.getMatrix();
					obj.regX = startRegX;
					obj.regY = startRegY;
				}

				var id = "id"+Math.abs(e.pointerID+1); // some pointers have negative ids
				// convert all pointer x and y to the parent container of the obj
				var local = obj.parent.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
				// we compare current pointer to start pointer (rather than increment as we go)
				obj.zimTouch.pointers[id] = {
					startX:local.x, startY:local.y,
					x:local.x, y:local.y
				};
				if (obj.zimTouch.move || obj.zimTouch.rotate) {
					obj.zimTouch.total = 0;
					zim.loop(obj.zimTouch.pointers, function(id) {
						obj.zimTouch.total++;
					});
				}
				if (slide && obj.zimTouch.total == 1) obj.zimTouch.slideInterval.pause(false);
				setTouches();
			})

			obj.zimTouch.pressmove = obj.on("pressmove", function(e) {

				var stage = obj.stage;

				var id = "id"+Math.abs(e.pointerID+1);
				var local = obj.parent.globalToLocal(e.stageX/stage.scaleX, e.stageY/stage.scaleY);
				// update our pointer data with new x and y
				if (obj.zimTouch.pointers[id]) {
					obj.zimTouch.pointers[id].x = local.x;
					obj.zimTouch.pointers[id].y = local.y;
				}

				// average the pointers' movement
				var newX = 0;
				var newY = 0;
				var aveX = 0; // point about which to scale and rotate
				var aveY = 0;
				var deltaX = 0;
				var deltaY = 0;
				zim.loop(obj.zimTouch.pointers, function(id, pointer) {
					newX += pointer.x - pointer.startX;
					newY += pointer.y - pointer.startY;
					aveX += pointer.x;
					aveY += pointer.y;
				});
				deltaX = newX / obj.zimTouch.total;
				deltaY = newY / obj.zimTouch.total;
				newX = obj.zimTouch.startX + deltaX;
				newY = obj.zimTouch.startY + deltaY;
				aveX = aveX / obj.zimTouch.total;
				aveY = aveY / obj.zimTouch.total;

				var proxy = { // thanks çŽ‹æ™“ä¸œ for the alert that we were missing this!
 					x:obj.x,
					y:obj.y,
					scaleX:obj.scaleX,
					scaleY:obj.scaleY,
					rotation:obj.rotation
				}; // will store desired scale and rotation on this object then apply matrix calculations

				if (obj.zimTouch.move) {
					var result = obj.zimTouch.checkBounds(newX, newY); // es6 opportunity
					proxy.x = result.x;
					proxy.y = result.y;
				}

				// if we have multitouch as determined by setTouches()
				if (obj.zimTouch.pair.length == 2) {
					var point1 = obj.zimTouch.pair[0];
					var point2 = obj.zimTouch.pair[1];
					if (obj.zimTouch.scale) {
						// use ratio of distance between fingers to start and then current distance between fingers
						var startDistance = Math.sqrt(Math.pow((point2.startX-point1.startX),2) + Math.pow((point2.startY-point1.startY),2));
						var currentDistance = Math.sqrt(Math.pow((point2.x-point1.x),2) + Math.pow((point2.y-point1.y),2));
						var newScaleX = obj.zimTouch.startSX + (currentDistance / startDistance - 1);
						var newScaleY = obj.zimTouch.startSY + (currentDistance / startDistance - 1);
						proxy.scaleX = newScaleX;
						proxy.scaleY = newScaleY;

						proxy.scaleX = dampScaleX.convert(newScaleX);
						proxy.scaleY = dampScaleY.convert(newScaleY);

						// set to scale min or max if scale would be outside range
						var minBad = (!zot(minScale) && Math.min(newScaleX, newScaleY) < minScale);
						var maxBad = (!zot(maxScale) && Math.max(newScaleX, newScaleY) > maxScale);
						if (minBad || maxBad) {
							if (minBad) {
								if (scaleRatio > 1) {
									proxy.scaleY = minScale;
									proxy.scaleX = minScale*scaleRatio;
								} else {
									proxy.scaleX = minScale;
									proxy.scaleY = minScale/scaleRatio;
								}
							} else if (maxBad) {
								if (scaleRatio > 1) {
									proxy.scaleX = maxScale;
									proxy.scaleY = maxScale/scaleRatio;
								} else {
									proxy.scaleY = maxScale;
									proxy.scaleX = maxScale*scaleRatio;
								}
							}
							dampScaleX.immediate(proxy.scaleX);
							dampScaleY.immediate(proxy.scaleY);
						}

					}
					if (obj.zimTouch.rotate) {
						// rotate based on the difference of angle between the fingers at start and at current
						var startAngle = Math.atan2((point1.startY - point2.startY), (point1.startX - point2.startX)) * (180 / Math.PI);
						var currentAngle = Math.atan2((point1.y - point2.y), (point1.x - point2.x)) * (180 / Math.PI);
						var deltaR = currentAngle - startAngle;
						proxy.rotation = obj.zimTouch.startR + deltaR;
					}

					if (regControl) {
						obj.scaleX = proxy.scaleX;
						obj.scaleY = proxy.scaleY;
						obj.rotation = proxy.rotation;
						if (obj.zimTouch.move) {
							obj.x = proxy.x
							obj.y = proxy.y
						}
					} else {
						// transformations seem to ignore registration so need to set to 0 then reset after transformations
						obj.reg(0,0);

						// need global data - the transformation about the pinch point is an adjustment of the system
						// originally we wanted calculations in the container of the object
						// these calculations were probably used for min and max of things, etc. so just leaving them
						// and instead, bringing the calculations back into the global - and then eventually into the local for the transformations
						var adjust = obj.parent.localToGlobal(aveX, aveY);
						aveX = adjust.x;
						aveY = adjust.y;

						// unfortunately, system goes haywire after a while without adjusting for minor shifts
						// saving the location of the center back to global after the transformation seems to correct this
						// we set lastPoint down below after the transformation
						var point = zot(lastPoint) ? obj.globalToLocal(aveX, aveY) : obj.globalToLocal(lastPoint.x, lastPoint.y);

						// the classic Matrix transformation - translate to the pinch point, scale and rotate, and then translate back
						// var matrix = new createjs.Matrix2D()
						var matrix = matrixStart.clone()
							.translate(point.x, point.y)
							.rotate(proxy.rotation-startRotation)
							.scale(proxy.scaleX/startScaleX,proxy.scaleY/startScaleY)
							.translate(-(point.x), -(point.y))
							// .prependMatrix(matrixStart) // would use if started new Matrix each time

						matrix.decompose(obj);

						if (obj.zimTouch.move) {
							obj.x += deltaX;
							obj.y += deltaY;
						}

						// correcting for minor shifts which can magnify and set the system haywire
						lastPoint = obj.localToGlobal(point.x, point.y);

						obj.reg(startRegX, startRegY);
					}

					if (obj.zimTouch.scale) obj.dispatchEvent("scale");
					if (obj.zimTouch.rotate) obj.dispatchEvent("rotate");
					if (obj.zimTouch.move) obj.dispatchEvent("move");

				} else {
					obj.x = proxy.x;
					obj.y = proxy.y;
					if (obj.zimTouch.move) obj.dispatchEvent("move");
				}
				if (obj.type == "Tag" || obj.type == "TextArea" || obj.type == "Loader") obj.resize();

				if (obj.ZIMoutlineShape) obj.outline();
				if (obj.getStage && obj.stage) obj.stage.update();
			});

			var mouseCount = 0;
			obj.on("mousedown", function() {
				// if an object is removed that is touched, the touch will still be registered
				// this code makes sure that no ghost touches are left by counting stagemousedown and stagemouseup events
				var mouseCount = 1;
				obj.stage.on("stagemousedown", function() {
					mouseCount++;
				});
				obj.stage.on("stagemouseup", function() {
					mouseCount--;
					if (mouseCount == 0) {
						setTimeout(function() {
							if (obj.zimTouch) obj.zimTouch.total = 0;
							if (obj.zimTouch) obj.zimTouch.pointers = {};
							maxTouches = null;
						}, 50);
					}
				});
			}, null, true); // once

			obj.zimTouch.pressup = obj.on("pressup", function(e) {
				var id = "id"+Math.abs(e.pointerID+1);
				// remove touch data for pointer
				delete obj.zimTouch.pointers[id];
				if (obj.zimTouch.move || obj.zimTouch.rotate) obj.zimTouch.total--;
				if (rotate && !zot(snapRotate) && obj.zimTouch.total == 0) {
					if (snapRotate > 0) {
						obj.rotation = Math.round(obj.rotation/snapRotate)*snapRotate;
					} else if (snapRotate == 0) {
						obj.rotation = Math.round(obj.rotation);
					}
				}
				// spinning was throwing slide off so just slide with single touch for now...
				if (slide && obj.zimTouch.total == 0 && maxTouches == 1) {
					obj.zimTouch.slideInterval.pause();
					var startSlide = slideData[(slideCount+1)%slideData.length];
					var currentSlide = slideData[(slideCount)%slideData.length];
					var newX = obj.x + (startSlide[0]-currentSlide[0]) * slideEffect;
					var newY = obj.y + (startSlide[1]-currentSlide[1]) * slideEffect;
					var result = obj.zimTouch.checkBounds(newX, newY); // es6 opportunity
					// if it is being thrown past the bounds, need to reduce time by percentage of blocked movement
					var newT = slideTotal*slideSlice*slideEffect * Math.min((obj.x-result.x)/(obj.x-newX)||1, (obj.y-result.y)/(obj.y-newY)||1);
					obj.animate({x:result.x, y:result.y}, newT, "quadOut", function(){obj.dispatchEvent("slidestop");});
				}
				if (obj.zimTouch.total == 0) maxTouches = null;
				if (obj.getStage && obj.stage) obj.stage.update();
				setTouches();
			});
			function setTouches() {
				// anytime we add or remove a pointer we reset the start positions
				// this handles cases where single touch would change the start position
				// and handles removing of one of the active pairs of pointers
				// to be replaced with another current pointer
				obj.zimTouch.pair = [];
				zim.loop(obj.zimTouch.pointers, function(id, pointer, i) {
					pointer.startX = pointer.x;
					pointer.startY = pointer.y;
					// just record the first two pointers
					// keep looping as mov uses all pointers
					if (i <= 1) obj.zimTouch.pair.push(pointer);
				});
				// record the start position, scale and rotation
				obj.zimTouch.startX = obj.x;
				obj.zimTouch.startY = obj.y;
				obj.zimTouch.startSX = obj.scaleX;
				obj.zimTouch.startSY = obj.scaleY;
				obj.zimTouch.startR = obj.rotation;
			}

			if (boundary) {
				remakeBoundary();
				obj.on("move", remakeBoundary);
			}
			function remakeBoundary() {
				var p = obj.localToGlobal(obj.regX, obj.regY);
				if (circularBounds) {
					// assuming cropped if Bitmap and width==height
					var leftTop = obj.parent.localToGlobal(obj.x-obj.width/2, obj.y-obj.height/2);
					var botRight = obj.parent.localToGlobal(obj.x+obj.width/2, obj.y+obj.height/2);
					var b = {
						x:leftTop.x,
						y:leftTop.y,
						width:botRight.x-leftTop.x,
						height:botRight.y-leftTop.y
					}
				} else {
					var b = obj.boundsToGlobal();
				}
				var r = {
					x:obj.zimTouch.boundaryStartX+p.x-b.x,
					y:obj.zimTouch.boundaryStartY+p.y-b.y,
					width:obj.zimTouch.boundaryStartW-b.width,
					height:obj.zimTouch.boundaryStartH-b.height
				};
				obj.gestureBoundary(r, false); // do not update the original rect
			}
		}
		return obj;
	}//-34.5

/*--
obj.noGesture = function(move, scale, rotate)

noGesture
zim DisplayObject method

DESCRIPTION
Removes multi-touch pan, pinch and rotation gestures from an object.
If all three are removed then deletes the zimTouch object and touch events from obj

EXAMPLE
rectangle.noGesture(); // removes all gestures
// or
rectangle.noGesture(true, true, false); // would leave rotation
// or with ZIM DUO
rectangle.noGesture({rotation:false}); // would leave rotation
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
move - (default true) - set to false not to remove move gesture
scale - (default true) - set to false not to remove scale gesture
rotate - (default true) - set to false not to remove rotate gesture

RETURNS obj for chaining
--*///+34.6
	zim.noGesture = function(obj, move, scale, rotate) {
		var sig = "obj, move, scale, rotate";
		var duo; if (duo = zob(zim.noGesture, arguments, sig)) return duo;
		z_d("34.6");

		if (zot(obj) || !obj.on || !obj.zimTouch) return;
		if (zot(move)) move = true;
		if (zot(scale)) scale = true;
		if (zot(rotate)) rotate = true;
		obj.zimTouch.move = !move;
		obj.zimTouch.scale = !scale;
		obj.zimTouch.rotate = !rotate;
		if (!obj.zimTouch.move && !obj.zimTouch.scale && !obj.zimTouch.rotate) {
			obj.off("mousedown", obj.zimTouch.mousedown);
			obj.off("pressmove", obj.zimTouch.pressmove);
			obj.off("pressup", obj.zimTouch.pressup);
			delete obj.zimTouch;
		}

		return obj;
	}//-34.6

/*--
obj.gestureBoundary = function(boundary, new)

gestureBoundary
zim DisplayObject method

DESCRIPTION
Dynamically changes or adds a boundary rectangle to the object being dragged with gesture().

EXAMPLE
var boundary = new Boundary(100,100,500,400); // x,y,w,h
circle.gestureBoundary(boundary);

OR pre ZIM 4TH
gestureBoundary(circle, boundary);
END EXAMPLE

PARAMETERS
boundary - is a ZIM Boundary rectangle - the local / global does not change from the original gesture setting
update - (default true) reset base drag boundary

RETURNS obj for chaining
--*///+34.7
	zim.gestureBoundary = function(obj, boundary, update) {
		z_d("34.7");
		if (zot(obj) || !obj.on) return;
		if (zot(boundary) || !obj.zimTouch) return;
		if (obj.zimTouch.localBounds && obj.parent) {
			obj.zimTouch.boundary = zim.boundsToGlobal(obj.parent, boundary);
		} else {
			obj.zimTouch.boundary = boundary;
		}
		if (zot(update)) update = true;
		if (update) {
			obj.zimTouch.boundaryStartX = boundary.x;
			obj.zimTouch.boundaryStartY = boundary.y;
			obj.zimTouch.boundaryStartW = boundary.width;
			obj.zimTouch.boundaryStartH = boundary.height;
		}
		var result = obj.zimTouch.checkBounds(obj.x, obj.y);
		obj.x = result.x;
		obj.y = result.y;
		return obj;
	}//-34.7

/*--
obj.addPhysics = function(dynamic, contract, shape, friction, linear, angular, density, restitution, maskBits, categoryBits, physics)

addPhysics
zim DisplayObject method

DESCRIPTION
Add Physics to a DisplayObject.  DisplayObject should be centerReg() on the stage (or in non-transformed Container at 0,0 on stage)
The Physics world can be set up with the ZIM Physics() class in the Controls module.
Box2D and ZIM physics JavaScript helper module need to be imported.
If no physics world has been created, the addPhysics() method will create a default world.
The default world will have a borders around the stage and gravity of 10.
See: https://zimjs.com/physics/

NOTE: Adding physics also adds more methods and properties to the DisplayObject
See the Physics() docs for all the details

EXAMPLE
new Circle().center().addPhysics(); // circle will fall with gravity to the floor
END EXAMPLE

EXAMPLE
// create a world with no gravity (viewed from top like air-hockey)
var physics = new Physics(0);

// create physics objects using the addPhysics() method
var circle = new Circle(50,blue,grey).center().addPhysics({restitution:1.1}); // how bouncy

// make sure to centerReg() any rectangular objects
var rect = new Rectangle(30,400).centerReg().pos(70).addPhysics(false); // static - do not move
var tri = new Triangle(150,150,150,green,grey).center().pos(200).addPhysics({linear:10}); // does not slide easily

// turn on dragging
physics.drag();
END EXAMPLE

PARAMETERS
dynamic - (default true) - set to true to not move the physics body (static)
contract - (default 0) - make the physics body smaller (or bigger with negative) than bounds
shape - (default object shape) - "rectangle" for any object other than Cirlce, Dial and Triangle
 	but can specify a "circle" for a Sprite or Bitmap, for instance - to try and match shape
	custom polygon bodies can also be made with manual Box2D and then use physics.setMap()
	but the only shapes available automatically are "rectangle", "circle", "triangle"
friction - (default .8) - how sticky will the body act - set to 0 to slide.
linear - (default .5) - linear damping which slows the movement - set to 0 for no damping
angular - (default .5) - angular damping which slows the rotation - set to 0 for no damping
density - (default 1) - the density that can affect what happens with collisions
restitution - (default 0) - how bouncy the object is - 0 is not bouncy 4 is crazy bouncy
maskBits - (default null) - used with categoryBits to determine which bodies will collide with which other bodies
	as soon as maskBits is specified, the body will collide only with the categoryBits provided to the maskBits parameter
	1 will collide with bodies that do not have categoryBits specified including the borders
	to test collision with bodies that have categoryBits specified, use the pipe (|) as follows:
	1|2 will also collide with bodies having categoryBits of 2 specified
	so if another body has categoryBits of 4 then the bodies would not collide.
	1|2|4 would also collide with 4 but not bodies with categoryBits of 8, etc.
	2|4 would pass through any bodies without categoryBits of 2 or 4 including the borders
categoryBits - (default 1) - a collision category - by default, bodies are in category 1
	use with maskBits to say which bodies will collide with which other bodies
	the values are bit fields https://en.wikipedia.org/wiki/Bit_field so can have the following decimal values:
	can be 2, 4, 8, 16, 32, 64, 128, 256, etc. up to 15 powers of 2
physics - (default zimDefaultPhysics)

RETURNS obj for chaining
--*///+34.8

	zim.addPhysics = function(obj, dynamic, contract, shape, friction, linear, angular, density, restitution, maskBits, categoryBits, physics) {
		var sig = "obj, dynamic, contract, shape, friction, linear, angular, density, restitution, maskBits, categoryBits, physics";
		var duo; if (duo = zob(zim.addPhysics, arguments, sig)) return duo;
		z_d("34.8");

		if (zot(physics)) {
			if (typeof(zimPhysics) == "undefined") {if (zon) {zog("ZIM Physics module must be imported")}; return obj;}
			if (zot(zimDefaultPhysics)) zimDefaultPhysics = new zim.Physics();
			physics = zimDefaultPhysics;
		}
		obj.physics = physics;
		var defaultShape = "rectangle";
		if (obj.type == "Circle" || obj.type == "Dial") defaultShape = "circle";
		else if (obj.type == "Triangle") defaultShape = "triangle";
		if (zot(shape)) shape = defaultShape;
		setPhysics(obj, dynamic, contract, shape, friction, linear, angular, density, restitution, maskBits, categoryBits);

		return obj;
	}//-34.8

/*--
obj.removedPhysics = function()

removePhysics
zim DisplayObject method

DESCRIPTION
Remove Physics from a DisplayObject.
This removes various methods and properties that addPhysics() added
The object can be added back to the physics world with addPhysics()

EXAMPLE
obj.removePhysics(); // ;-)
END EXAMPLE

RETURNS obj for chaining
--*///+34.85
	zim.removePhysics = function(obj) {
		z_d("34.85");
		if (zot(obj.physics)) return obj;
		if (obj == obj.physics.followObj) obj.physics.follow(null);
		obj.physics.remove(obj.body);
		obj.follow = obj.noFollow = obj.control = obj.noControl = obj.sleep = obj.wake = null;
		obj.physics = obj.impulse = obj.force = obj.spin = obj.torque = null;
		return obj;
	}//-34.85

// SUBSECTION HIT TESTS

/*--
obj.hitTestPoint = function(x, y, boundsCheck)

hitTestPoint
zim DisplayObject method

DESCRIPTION
See if shape of obj is hitting the global point x and y on the stage.

EXAMPLE
var circle = new Circle();
stage.addChild(circle);
circle.drag();
circle.on("pressmove", function() {
	if (circle.hitTestPoint(stageW/2, stageH/2)) {
		if (circle.alpha == 1) {
			circle.alpha = .5;
			stage.update();
		}
	} else {
		if (circle.alpha == .5) {
			circle.alpha = 1;
			stage.update();
		}
	}
});
END EXAMPLE

PARAMETERS
x and y - the point we are testing to see if it hits the shape of the object
boundsCheck (default true) do a bounds check first (faster) but if bounds are wrong - might not work

RETURNS a Boolean true if hitting, false if not
--*///+35
	zim.hitTestPoint = function(obj, x, y, boundsCheck) {
		z_d("35");
		if (!obj.stage || zot(x) || zot(y)) return false;
		var stage = obj.stage;
		if (zot(obj) || !obj.globalToLocal) return;
		if (zot(boundsCheck)) boundsCheck = true;
		if (boundsCheck) {
			var bounds = obj.getBounds();
			if (bounds) { // faster to check if point is in bounds first
				var boundPoint = obj.globalToLocal(x,y);
				if (boundPoint.x > bounds.x + bounds.width || boundPoint.x < bounds.x) return false;
				if (boundPoint.y > bounds.y + bounds.height || boundPoint.y < bounds.y) return false;
			}
		}
		// shape is scaled as the stage so multiply x and y by stage scale
		x*=stage.scaleX;
		y*=stage.scaleY;
		var point = obj.globalToLocal(x,y);
		return obj.hitTest(point.x, point.y);
	}//-35

/*--
obj.hitTestReg = function(other, boundsCheck)

hitTestReg
zim DisplayObject method

DESCRIPTION
See if the shape shape of an object is hitting the registration point of object (other).

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage);
circle.drag();
var rect = new Rectangle(100, 100, "blue");
stage.addChild(rect);
circle.on("pressmove", function() {
	if (circle.hitTestReg(rect)) {
		stage.removeChild(rect);
		stage.update();
	}
})
END EXAMPLE

PARAMETERS
other - the object whose registration point we are checking against
boundsCheck (default true) do a bounds check first (faster) but if bounds are wrong - might not work

RETURNS a Boolean true if hitting, false if not
--*///+36
	zim.hitTestReg = function(a, b, boundsCheck) {
		z_d("36");
		if (!a.stage || !b.stage) return false;
		var stage = a.stage;
		if (zot(a) || zot(b) || !a.localToLocal || !b.localToLocal) return;
		if (zot(boundsCheck)) boundsCheck = true;

		var point = b.localToGlobal(b.regX,b.regY);
		var bounds = a.getBounds();
		if (boundsCheck && bounds) { // faster to check if point is in bounds first
			// note, bounds get pre-scaled x and y - only hitTest() needs scaled
			var boundPoint = a.globalToLocal(point.x,point.y);
			if (boundPoint.x > bounds.x + bounds.width || boundPoint.x < bounds.x) return false;
			if (boundPoint.y > bounds.y + bounds.height || boundPoint.y < bounds.y) return false;
		}
		point = a.globalToLocal(point.x*stage.scaleX,point.y*stage.scaleY);
		return a.hitTest(point.x, point.y);
	}//-36

/*--
obj.hitTestRect = function(other, num, boundsCheck)

hitTestRect
zim DisplayObject method

DESCRIPTION
See if a shape of an object is hitting points on a rectangle of another object.
The rectangle is based on the position, registration and bounds of object (other).
num is how many points on the edge of the rectangle we test - default is 0.
The four corners are always tested as well as the very middle of the rectangle.

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage);
circle.drag();
var rect = new Rectangle(100, 100, "blue");
stage.addChild(rect);
circle.on("pressmove", function() {
	if (circle.hitTestRect(rect)) {
		stage.removeChild(rect);
		stage.update();
	}
});
END EXAMPLE

PARAMETERS
other - the object whose bounding rectangle we are checking against
num - (default 0) the number of points along each edge to check
	1 would put a point at the middle of each edge
	2 would put two points at 1/3 and 2/3 along the edge, etc.
	there are always points at the corners
	and one point in the middle of the rectangle
boundsCheck (default true) do a bounds check first (faster) but if bounds are wrong - might not work

RETURNS a Boolean true if hitting, false if not
--*///+37
	zim.hitTestRect = function(a, b, num, boundsCheck) {
		z_d("37");
		if (!a.stage || !b.stage) return false;
		var stage = a.stage;
		if (zot(a) || zot(b) || !a.hitTest || !b.getBounds) return;
		if (zot(num)) num = 0;
		if (zot(boundsCheck)) boundsCheck = true;
		var bounds = b.getBounds();
		if (!bounds) {
			zog("zim methods - hitTestRect():\n please setBounds() on param b object");
			return;
		}
		if (boundsCheck) {
			var bounds2 = a.getBounds();
			if (bounds2 && !zim.hitTestBounds(a,b)) return false; // bounds not hitting
		}
		function test(point) {
			point = a.globalToLocal(point.x*stage.scaleX,point.y*stage.scaleY);
			if (a.hitTest(point.x, point.y)) return true;
		}
		var centerX = bounds.x+bounds.width/2;
		var centerY = bounds.y+bounds.height/2;
		var point = b.localToGlobal(centerX, centerY);
		if (test(point)) return true; // check hit on center of Rectangle

		var shiftX, shiftY, point;
		//num = 0;  1/1
		//num = 1;  1/2  2/2
		//num = 2;  1/3  2/3  3/3
		//num = 3;  1/4  2/4  3/4  4/4

		for (var i=0; i<=num; i++) {
			shiftX = bounds.width  * (i+1)/(num+1);
			shiftY = bounds.height * (i+1)/(num+1);
			point = b.localToGlobal(bounds.x+shiftX, bounds.y);
			if (test(point)) return true;
			point = b.localToGlobal(bounds.x+bounds.width, bounds.y+shiftY);
			if (test(point)) return true;
			point = b.localToGlobal(bounds.x+bounds.width-shiftX, bounds.y+bounds.height);
			if (test(point)) return true;
			point = b.localToGlobal(bounds.x, bounds.y+bounds.height-shiftY);
			if (test(point)) return true;
		}
	}//-37

/*--
obj.hitTestCircle = function(other, num, boundsCheck)

hitTestCircle
zim DisplayObject method

DESCRIPTION
See if the shape of an object is hitting points on a circle of another object.
The circle is based on the position, registration and bounds of object (other).
num is how many points around the circle we test - default is 8
Also checks center of circle hitting.

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage);
circle.drag();
var triangle = new Triangle(100, 100, 100, "blue");
stage.addChild(triangle);
circle.on("pressmove", function() {
	if (triangle.hitTestCircle(circle)) {
		stage.removeChild(triangle);
		stage.update();
	}
});
END EXAMPLE

PARAMETERS
other - the object whose circle based on the bounding rect we are using
num - (default 8) the number of points evenly distributed around the circle
	and one point in the middle of the circle
boundsCheck (default true) do a bounds check first (faster) but if bounds are wrong - might not work

RETURNS a Boolean true if hitting, false if not
--*///+38
	zim.hitTestCircle = function(a, b, num, boundsCheck) {
		z_d("38");
		if (!a.stage || !b.stage) return;
		stage = a.stage;
		if (zot(a) || zot(b) || !a.hitTest || !b.getBounds) return;
		if (zot(num)) num = 8;
		if (zot(boundsCheck)) boundsCheck = true;
		var bounds = b.getBounds();
		if (!bounds) {
			zog("zim methods - hitTestCircle():\n please setBounds() on param b object");
			return;
		}
		var bounds2 = a.getBounds();
		if (boundsCheck && bounds2 && !zim.hitTestBounds(a,b)) return false; // bounds not hitting
		function test(point) {
			point = a.globalToLocal(point.x*stage.scaleX,point.y*stage.scaleY);
			if (a.hitTest(point.x, point.y)) return true;
		}
		var centerX = bounds.x+bounds.width/2;
		var centerY = bounds.y+bounds.height/2;
		var point = b.localToGlobal(centerX, centerY);
		if (test(point)) return true; // check hit on center of circle
		var radius = (bounds.width+bounds.height)/2/2; // average diameter / 2
		var angle, pointX, pointY;
		for (var i=0; i<num; i++) {
			angle = i/num * 2*Math.PI; // radians
			pointX = centerX + (radius * Math.cos(angle));
			pointY = centerY + (radius * Math.sin(angle));
			point = b.localToGlobal(pointX, pointY);
			if (test(point)) return true;
		}

	}//-38

/*--
obj.hitTestCircles = function(other, margin)

hitTestCircles
zim DisplayObject method

DESCRIPTION
Uses an equation to see if two circles are intersecting.
This is faster than hitTests on shapes - so will have the speed of hitTestBounds and hitTestGrid.
The circles are based on the bounds of the two objects - it does not matter on which object the method is placed.
If the bounds are not square then half the average length of the sides is used as the radius.
A margin parameter is provided to tweak the hitTest

EXAMPLE
var ball = new Circle(50, "red");
ball.center(stage);
ball.drag();
var basket = new Circle(100, "blue");
stage.addChild(basket);
ball.on("pressmove", function() {
	if (ball.hitTestCircles(basket)) {
		zog("points!");
	}
});
END EXAMPLE

PARAMETERS
other - the object whose circle based on the bounding rect we are using
margin (default 0) pixels the distance between circles is increased or decreased to effect the hit

RETURNS a Boolean true if hitting, false if not
--*///+38.5
	zim.hitTestCircles = function(a, b, margin) {
		z_d("38.5");
		if (!a.stage || !b.stage) return;
		var stage = a.stage;
		if (zot(a) || zot(b) || !a.hitTest || !a.getBounds || !b.getBounds) return;
		var aBounds = a.getBounds();
		var bBounds = b.getBounds();
		if (!aBounds || !bBounds) {
			zog("zim methods - hitTestCircles():\n please setBounds() on both objects");
			return;
		}
		if (zot(margin)) margin = 0;
		var p1 = a.localToGlobal(aBounds.x + aBounds.width/2, aBounds.y + aBounds.height/2);
		var p2 = b.localToGlobal(bBounds.x + bBounds.width/2, bBounds.y + bBounds.height/2);
		var m1 = a.getConcatenatedMatrix();
		var m2 = b.getConcatenatedMatrix();
		scale1X = Math.sqrt((m1.a/stage.scaleX * m1.a/stage.scaleX) + (m1.c/stage.scaleX * m1.c/stage.scaleX));
		scale1Y = Math.sqrt((m1.b/stage.scaleY * m1.b/stage.scaleY) + (m1.d/stage.scaleY * m1.d/stage.scaleY));
		scale2X = Math.sqrt((m2.a/stage.scaleX * m2.a/stage.scaleX) + (m2.c/stage.scaleX * m2.c/stage.scaleX));
		scale2Y = Math.sqrt((m2.b/stage.scaleY * m2.b/stage.scaleY) + (m2.d/stage.scaleY * m2.d/stage.scaleY));
		var d = Math.sqrt(Math.abs(Math.pow(p1.x-p2.x, 2)+Math.pow(p1.y-p2.y, 2)));
		if (aBounds.width*Math.max(scale1X, scale1Y)/2 + bBounds.width*Math.max(scale2X, scale2Y)/2 >= d-margin) return true;
		else return false;

	}//-38.5

/*--
obj.hitTestBounds = function(other, margin, boundsShape)

hitTestBounds
zim DisplayObject method

DESCRIPTION
See if obj.getBounds() is hitting other.getBounds().
Margin can be adjusted to tweak the hitTest.
Pass in a boundsShape shape if you want a demonstration of where the bounds are.

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage);
circle.drag();
var rect = new Rectangle(100, 100, "blue");
stage.addChild(rect);
circle.on("pressmove", function() {
	if (circle.hitTestBounds(rect)) {
		stage.removeChild(rect);
		stage.update();
	}
});
END EXAMPLE

PARAMETERS
other - another object whose rectanglular bounds we are testing
margin (default 0) shifted distance in pixels before hit is counted - can be positive or negative
boundsShape - (default null) an empty Shape or createjs.Shape
	you would need to add the boundsShape to the stage

RETURNS a Boolean true if hitting, false if not
--*///+39
	zim.hitTestBounds = function(a, b, margin, boundsShape) {
		z_d("39");
		if (!a.stage || !b.stage) return;
		if (zot(a) || zot(b) || !a.getBounds || !b.getBounds) return;
		var boundsCheck = false;
		if (boundsShape && boundsShape.graphics) boundsCheck=true;

		var aB = a.getBounds();
		var bB = b.getBounds();
		if (!aB || !bB) {
			zog("zim methods - hitTestBounds():\n please setBounds() on both objects");
			return;
		}
		if (zot(margin)) margin = 0;
		var adjustedA = zim.boundsToGlobal(a);
		var adjustedB = zim.boundsToGlobal(b);

		if (boundsCheck) {
			var g = boundsShape.graphics;
			g.c();
			g.ss(1).s("blue");
			g.r(adjustedA.x, adjustedA.y, adjustedA.width, adjustedA.height);
			g.s("green");
			g.r(adjustedB.x, adjustedB.y, adjustedB.width, adjustedB.height);
			boundsShape.stage.update();
		}
		return zim.rectIntersect(adjustedA, adjustedB, margin);

	}//-39

//
/*--
obj.hitTestPath = function(other, num, showPoints)

hitTestPath
zim DisplayObject method

DESCRIPTION
See if the shape of an object is hitting points on a path of a Squiggle or Blob.
num is how many points between each point on the path we test - default is 2

EXAMPLE
var path = new Blob().center();
var circle = new Circle(50, purple).pos(100,100).drag();
circle.on("pressmove", function() {
	if (circle.hitTestPath(path, 3, true)) {
		circle.alpha -= .02;
		stage.update();
	}
});
END EXAMPLE

PARAMETERS
other - the path (Squiggle or Blob) to test to see if the object shape is hitting
num - (default 2) the number of points added to the path roughly between each point
	this gets totalled - so num*numPoints and then distributed along the path
showPoints (default false) will draw small circles along the path where the hits will be tested

RETURNS a Boolean true if hitting, false if not
--*///+37.5
	zim.hitTestPath = function(a, b, num, showPoints) {
		z_d("37.5");
		if (!a.stage || !b.stage) return;
		stage = a.stage;
		if (zot(a) || zot(b) || !a.hitTest) return;
		if (b.type != "Squiggle" && b.type != "Blob") return;
		var aB = a.getBounds();
		if (zot(num)) num = 2;
		if (zot(showPoints)) showPoints = false;

		function test(point) {
			point = a.globalToLocal(point.x*stage.scaleX,point.y*stage.scaleY);
			if (a.hitTest(point.x, point.y)) return true;
		}
		var points = b.interpolate(num, null, true);
		if (showPoints) {
			if (!b.hitPathPoints) b.hitPathPoints = new Container().addTo(stage);
			else b.hitPathPoints.removeAllChildren();
			b.hitPathPoints.top();
			for (var i=0; i<points.length; i++) {
				var point = points[i];
				point = b.localToGlobal(point.x, point.y);
				new zim.Circle(3).loc(point.x, point.y, b.hitPathPoints);
			}
		}
		if (aB) {
			var bounds = zim.boundsAroundPoints(points);
			if (!zim.rectIntersect(a.boundsToGlobal(aB), b.boundsToGlobal(bounds))) return;
		}

		for (var i=0; i<points.length; i++) {
			var point = points[i];
			point = b.localToGlobal(point.x, point.y);
			if (test(point)) return true;
		}
		return false;
	}//-37.5

/*--
obj.hitTestGrid = function(width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type)

hitTestGrid
zim DisplayObject method

DESCRIPTION
Converts an x and y point to an index in a grid.
If you have a grid of rectangles, for instance, use this to find out which rectangle is beneath the cursor.
This technique will work faster than any of the other hit tests.

EXAMPLE
var tile = new Tile(new Rectangle(100,100),5,4,10,10).center();
var circle = new Circle(10,green).addTo().drag();
circle.on("pressmove", function (e) {
	var index = tile.hitTestGrid(tile.width, tile.height, 5, 4, circle.x, circle.y,0,0,10,10);
	zog(index);
});
END EXAMPLE

PARAMETERS
width and height - the overall dimensions
cols and rows - how many of each (note it is cols and then rows)
x and y - where you are in the grid (eg. e.stageX and e.stageY)
offsetX and offsetY - (default 0) the distances the grid starts from the origin of the obj
spacingX and spacingY - (default 0) spacing between grid cells (null will be returned if x and y within spacing)
	spacing is only between the cells and is to be included in the width and height (but not outside the grid)
local - (default false) set to true to convert x and y to local values
type - (default index) which means the hitTestGrid returns the index of the cell beneath the x and y point
	starting with 0 at top left corner and counting columns along the row and then to the next row, etc.
	set type to "col" to return the column and "row" to return the row
	set to "array" to return all three in an Array [index, col, row]

RETURNS an index Number (or undefined) | col | row | an Array of [index, col, row]
--*///+41
	zim.hitTestGrid = function(obj, width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type) {
		z_d("41");
		if (!obj.stage) return false;
		if (!zot(obj) && !local) {
			var point = obj.globalToLocal(x,y);
			x=point.x; y=point.y;
		}
		if (zot(offsetX)) offsetX = 0;
		if (zot(offsetY)) offsetY = 0;
		if (zot(spacingX)) spacingX = 0;
		if (zot(spacingY)) spacingY = 0;

		width += spacingX;
		height += spacingY

		// assume spacing is to the right and bottom of a cell
		// turning this into an object would avoid the size calculations
		// but hopefully it will not be noticed - and then hitTests are all functions
		var sizeX = width / (cols);
		var sizeY = height / (rows);

		// calculate col and row
		var col = Math.min(cols-1,Math.max(0,Math.floor((x-offsetX)/sizeX)));
		var row = Math.min(rows-1,Math.max(0,Math.floor((y-offsetY)/sizeY)));

		// check if within cell
		if ((x-offsetX)>sizeX*(col+1)-spacingX || (x-offsetX)<sizeX*(col)) return;
		if ((y-offsetY)>sizeY*(row+1)-spacingY || (y-offsetY)<sizeY*(row)) return;

		var index = row*cols + col;
		if (zot(type) || type=="index") return index
		if (type == "col") return col;
		if (type == "row") return row;
		if (type == "array") return [index, col, row];
	}//-41

// SUBSECTION ANIMATE, WIGGLE, LOOP

/*--
obj.animate = function(props, time, ease, call, params, wait, waitedCall, waitedParams, loop, loopCount, loopWait, loopCall, loopParams, loopWaitCall, loopWaitParams, rewind, rewindWait, rewindCall, rewindParams, rewindWaitCall, rewindWaitParams, sequence, sequenceCall, sequenceParams, sequenceReverse, ticker, cjsProps, css, protect, override, from, set, id, events, sequenceTarget, dynamic, drag, clamp, startPaused, clean, obj)

animate
zim DisplayObject method
powered by createjs.Tween (TweenJS)

DESCRIPTION
Animate object obj properties in time milliseconds.
You can set various types of easing like bounce, elastic, back, linear, sine, etc.
Handles callbacks, delays, loops, rewinds, relative, series and sequences of animations.
SEE: https://zimjs.com/animation

Shape Animation
ZIM animate() lets you animate one Squiggle to another or one Blob to another
See the props parameter under the convenience properties for shape and blobShift

Advanced Animation
ZIM animate() lets you animate along a path made with a zim Squiggle or Blob
These paths can be edited by the user and the animation will still work
The paths themselves can be animated or wiggled.
Orient and Flipping are available.
Dynamic speed can be set with percentSpeed and tied in to Accelerator and MotionController
Scrubbing animation and path animation is also supported with percentComplete
This can be used in Parallax with a Slider, Dial, MotionController, or general coding
Dragging along a path is as easy as setting the drag parameter to true
This can be done with while animating or with the animation paused
ZIM EXTRA! provides animation based on animation.
This allows for setting zoom, depth, speed, fade, etc. based on target y value while animating on a path
but EXTRA! also opens up endless possibilities as the input and output does not have to be the target.
This means that animation can also control properties of other objects.
SEE: https://zimjs.com/nio.html

NOTE: to temporarily prevent animations from starting set ANIMATE to false
NOTE: see pauseAnimate(state, ids) and stopAnimate(ids) for controlling tweens when running
NOTE: set mouseEnabled of target before calling animate as animate itself sets mouseEnabled and then resets to original after a delay

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage);
circle.alpha = 0;
circle.sca(0);
circle.animate({alpha:1, scale:1}, 700, null, done);
function done(target) {
	// target is circle if params is not set
	target.drag();
}

// or with ZIM DUO and from parameter:
var circle = new Circle(50, "red");
circle.center(stage);
circle.animate({props:{alpha:0, scale:0}, time:700, from:true});

// note: there was no need to set alpha and scale to 0 before the animation
// because from will animate from property values in props {alpha:0, scale:0}
// to the present set values - which are 1 and 1 for the default scale and alpha.
// This allows you to place everything how you want it to end up
// and then easily animate to this state.
// An extra advantage of this is that you can use the ANIMATE constant to skip animations while building
// See the https://zimjs.com/ornamate.html example

// RELATIVE animation
// rotate the rectangle 360 degrees from its current rotation
rectangle.animate({rotation:"360"}, 1000);

// pulse circle
var circle = new Circle(50, "red");
circle.center(stage);
// pulse circle from scale 0 - 1 every second (use ZIM DUO)
circle.animate({props:{scale:0}, time:500, loop:true, rewind:true, from:true});
// toggle pause the circle when stage is pressed
stage.on("stagemousedown", function() {
	circle.pauseAnimate(!circle.paused);
});
END EXAMPLE

EXAMPLE
// using ZIM VEE value:
// this will animate the alpha to between .5 and 1 in either 1000ms or 2000ms
circle.animate({alpha:{min:.5, max:1}}, [1000, 2000]);
END EXAMPLE

EXAMPLE
// Dynamic Animation
var rect = new Rectangle(200,200,red)
	.centerReg()
	.animate({
		props:{rotation:360},
		loop:true,
		time:2000,
		ease:"linear",
		dynamic:true,
		set:{percentSpeed:0} // no speed to start
	});

// example using a Slider to set speed from 0 to 5 times as fast
var slider = new Slider(0,500).pos(100, 100);
slider.on("change", function () {
	rect.percentSpeed = slider.currentValue;
});

// example using an Accelerator and MotionController
// to set speed from -200 to 200 percent
// depending on mouse position
// multiple targets including Dynamo and Scroller objects can be added to Accelerator
// if adding multiple objects, use an array new Accelerator([rect, otherObject, anotherObject])
new MotionController({
    target:new Accelerator(rect),
    type:"mousemove",
    minPercentSpeed:-200,
    maxPercentSpeed:200
});
END EXAMPLE

EXAMPLE
// Series example animating a circle in square formation
// Also showing that the series can include multiple targets
// Click on the stage to pause or unpause the animation

var rect = new Rectangle({color:pink})
	.centerReg(stage)
	.sca(0); // hiding it to start

var circle = new Circle({color:purple}) // chaining the rest
	.addTo(stage)
	.pos(400,300)
	.animate({ // circle will be the default object for the inner animations
		props:[
			// an array of animate configuration objects
			{props:{x:600, y:300, scale:2}},
			{props:{x:600, y:500, scale:1}, call:function(){zog("part way");}},
			{props:{x:400, y:500}, time:500, ease:"quadInOut"},
			{target:rect, props:{scale:3}, time:1000, rewind:true, ease:"quadInOut"},
			{props:{x:400, y:300}}
		],
		time:1000, // will be the default time for the inner animations
		ease:"backOut", // will be the default ease for the inner animations
		id:"square", // will override any id set in the inner animations
		loop:true,
		loopCount:3,
		// note - no rewind or from parameters
		call:function(){zog("done");}
	});

	var paused = false;
	stage.on("stagemousedown", function() {
			paused = !paused;
			pauseAnimate(paused, "square");
	});
END EXAMPLE

EXAMPLE
// sequence example to pulse two circles
var circles = new Container(stageW, stageH).addTo(stage);
var circle1 = new Circle(50, "red").center(circles);
var circle2 = new Circle(50, "blue").center(circles).mov(70);
circles.animate({
	props:{scale:1},
	time:500,
	loop:true,
	rewind:true,
	from:true,
	sequence:500
});
END EXAMPLE

EXAMPLE
// Animate along a Squiggle or Blob path
// see https://zimjs.com/explore/squiggleAnimate.html for more
// see https://zimjs.com/explore/blobAnimate.html for more
// see https://zimjs.com/explore/blobAnimate2.html for more
var line = new Squiggle().center();
new Circle(10, red).addTo().animate({path:line}, 1000);
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function

props - the object literal holding properties and values to animate
	Basic examples: {x:200} or {rotation:360, alpha:0} or {scale:4} or {x:300, y:300, scale:"2"} (relative scale)
	There are custom options below including Convenience, ZIM VEE, Relative, and Series properties.
	** Before ZIM 7.1, this parameter was called obj - as to not conflict with CreateJS TweenJS props (now renamed cjsProps)
	   obj is still available as a parameter name for backwards compatibility when using a ZIM DUO configuration object
	CONVENIENCE PROPERTIES
		scale - for scaleX and scaleY
		color - on ZIM shapes for setColorRange() and animate colorRange from 0-1
	 		** this property cannot be run in a series - rather animate in a call function to accomplish a series of color changes
		shape - animate from one ZIM Squiggle to another or one ZIM Blob to another
			for now, the matching Sqiggle or Blob must have the same number of points
			to pause or stop a shape tween use the animate id parameter
		blobShift - a number of points to shift the points of a Blob in clockwise direction
			this is when using a shape tween that is a Blob - can be negative
		path - pass in a Blob or Squiggle to animate along path
			https://zimjs.com/nio/path.html
			If the shape has lockControls set to false (the default) then the animation will be dynamic
			and adjust as the shape is changed.
			If a set animation path is desired then set the shape's lockControls to true
		orient - an object with x and y to aim the target at
			https://zimjs.com/nio/orient.html
			when a path is used this will default to rotate to orient to path
			can set to false for no rotation
			can set to any object with an x, y point such as a circle on the stage - or {x:0, y:0} or a new zim Point, etc.
		flip - setting flip to true will flip the object horizontally when its x direction goes negative
			https://zimjs.com/nio/flip.html
			this will also work with orient to make sure the target is not upside down
		verticalFlip - setting verticalFlip to true will flip the object vertically when its y direction goes negative
			this will also work with orient to make sure the target is not upside down

		EXTRA! - the following convenience properties are available for ZIM EXTRA! (also see extra below)
			https://zimjs.com/nio/extra.html
			zoom - an array that represents scale proportions based on target's y position
				[inputMin, inputMax, outputMin(default 0), outputMax(defaunt stageH)] the outputs will be constrained to the provided values
				zoom:[.5,1.5] scales the target based on its y position - animating y or along a path will show scaling
				zoom:10 - is like zoom:[0,10]
				zoom:true - is like zoom:[0,1]
				zoom:[1,3,stageH/2-100,stageH/2+200] would scale from 1 to 3 in this region and stay at 1 or 3 outside this region
			speed - an array that represents animation percentSpeed based on target's y position
				[inputMin, inputMax, outputMin(default 0), outputMax(defaunt stageH)] the outputs will be constrained to the provided values
				speed:[20,100] sets percentSpeed based on target y position - animating y or along a path will have changing speed
				speed:40 - is like speed:[0,40]
				speed:true - is like speed:[0,100]
				** will set dynamic property to true - as percentSpeed needs dynamic property set to true
			layer - an array that represents ratio of layer proportions based on target's y position
				[inputMin, inputMax, outputMin(default 0), outputMax(defaunt stageH)] the outputs will be constrained to the provided values
				layer:[0,1] sets current layer based on target y position - animating y or along a path will have changing layers
				layer:1 - is like layer:[0,1]
				layer:true - is like layer:[0,target.parent.numChildren-1]
			fade - an array that represents ratio of alpha based on target's y position
				[inputMin, inputMax, outputMin(default 0), outputMax(defaunt stageH)] the outputs will be constrained to the provided values
				fade:[.5,1] sets current alpha based on target y position - animating y or along a path will have changing alpha
				fade:1 - is like fade:[0,1]
				fade:true - is like fade:[0,1]
				fade:[0,1,stageH/2-100,stageH/2+200] - would change alpha in this y region
		extra - in addition to the convenience properties above, ZIM EXTRA! has a more general and complete format:
			Pass in a single EXTRA! object or an array of EXTRA! objects: extra:{} or extra:[{},{},{}]
			The object has the following properties - all are optional except the outputProp which is required:
				|ZIM VEE| - each object below optionally accepts a ZIM VEE value for Pick.choose() to pick randomly from
		        inputObj - (default target) - the object with the input property - probably the animation target but could be any object
		        inputProp - (default "y") - a string of the property name for the input - "x", "rotation", etc.
		        inputMin - (default 0) - the minimum input value for the calculation - also see constrain
		        inputMax - (default stageW if x inputProp else stageH) - the maximum input value for the calculation - also see constrain
		        outputObj - (default target) - the object whose output property is being changed - probably the animation target but could be any object
		        outputProp - (default "scale") - a string of the property name of the output - "scale", "layer", "rotation", etc.
		        outputMin - (default 0) - the minimum output value for the calculation - also see constrain
		        outputMax - (default 1) - the maximum output value for the calculation - also see constrain
		        factor - (default 1) setting factor to -1 will reverse the direction of the animation
		        outputRound - (default false) set to true to receive rounded output values
				constrain - (default true) constrain the output value to outputMin and outputMax
					set to false to let values go beyond provided mins and maxes
					this is quite usual when a proportion is easily figured at a certain range
					and continued increase or decrease is desired outside the range - just turn constrain to false.
			EXAMPLES:
			extra:{outputProp:"scaleX"} would animate the scaleX of the target from 0-1 over the stage height
			extra:{outputObj:circle, outputProp:"alpha"} would animate the alpha of circle based on the animated target's y position
			extra:{inputObj:circle, inputProp:"x", inputMax:stageW, outputProp:"level"} would animate the target's child index as the circle's x goes across the screen
			** in the last two examples, circle is a different object than the target of the animation - circle might be animating independently or based on a Slider value, etc.
	|ZIM VEE| - each props property value optionally accepts a ZIM VEE value for Pick.choose() to pick randomly from (except the EXTRA! properties which accept ZIM VEE values inside)
	RELATIVE VALUES: you can pass in relative values by putting the numbers as strings
		rotation:"360" will animate the rotation of the object 360 degrees from its current rotation
		whereas rotation:360 will animate the rotation of the object to 360 degrees
	ANIMATION SERIES: if you pass in an array for the props value, then this will run an animation series
		The array must hold animate configuration objects:
		[{props:{scale:2}, time:1000, rewind:true}, {target:different, props:{x:100}}, etc.]
		If you run animate as a method on an object then this is the default object for the series
		but you can specify a target to override the default
		The default time and tween are as provided in the main parameters
		but you can specify these to override the default
		The id of the main parameters is used for the whole series and cannot be overridden
		The override parameter is set to false and cannot be overridden
		All other main parameters are available except rewind, sequence and from
		(rewind and from are available on the inner tweens - for sequence: the initial animation is considered)
		You currently cannot nest animimation series
		Note: if any of the series has a loop and loops forever (a loopCount of 0 or no loopCount)
		then this will be the last of the series to run
		Note: color cannot be animated in a series - rather animate in a call function to accomplish a series of color changes
		Note: a sequence cannot be seriesed and a series cannot be sequenced
time - |ZIM VEE| the time for the tween in milliseconds 1000 ms = 1 second
ease - |ZIM VEE| (default "quadInOut") the equation type for easing ("bounceOut", "elasticIn", "backInOut", "linear", etc)
	see CreateJS easing: https://www.createjs.com/docs/tweenjs/classes/Ease.html
call - (default null) the function to call when the animation is done
params - (default target) a single parameter for the call function (eg. use object literal or array)
wait - |ZIM VEE| (default 0) milliseconds to wait before doing animation
waitedCall - (default null) calls function after wait is done if there is a wait
waitedParams - (default target) parameters to send waitedCall function
loop - (default false) set to true to loop animation
loopCount - |ZIM VEE| (default 0) if loop is true how many times it will loop (0 is forever)
loopWait - |ZIM VEE| (default 0) milliseconds to wait before looping
loopCall - (default null) calls function after loop and loopWait (not including last loop)
loopParams - (default target) parameters to send loopCall function
loopWaitCall - (default null) calls function after at the start of loopWait
loopWaitParams - (default target) parameters to send loopWaitCall function
rewind - |ZIM VEE| (default false) set to true to rewind (reverse) animation (doubles animation time)
rewindWait - |ZIM VEE| (default 0) milliseconds to wait in the middle of the rewind
rewindCall - (default null) calls function at middle of rewind after rewindWait
rewindParams - (default target) parameters to send rewindCall function
rewindWaitCall - (default null) calls function at middle of rewind before rewindWait
rewindWaitParams - (default target) parameters to send rewindCall function
sequence - (default 0) the delay time in milliseconds to run on children of a container or an array of target animations
	with the addition of ZIM VEE object to the target, you must noPick the array
	for example, target = container or target = {noPick:[a,b,c]} and sequence = 1000
	would run the animation on the first child and then 1 second later, run the animation on the second child, etc.
	or in the case of the array, on element a and then 1 second later, element b, etc.
	If the loop prop is true then sequenceCall below would activate for each loop
	For an array, you must use the zim function with a target parameter - otherwise you can use the ZIM 4TH method
	Note: a sequence cannot be seriesed and a series cannot be sequenced
sequenceCall - (default null) the function that will be called when the sequence ends
sequenceParams - (default null) a parameter sent to the sequenceCall function
sequenceReverse - |ZIM VEE| (default false) set to true to sequence through container or array backwards
ticker - (default true) set to false to not use an automatic Ticker function
cjsProps - (default {override: true}) legacy - allows you to pass in CreateJS TweenJS configuration properties
css - (default false) set to true to animate CSS properties in HTML
 	requires CreateJS CSSPlugin
	ZIM has a copy here: https://d309knd7es5f10.cloudfront.net/CSSPlugin01.js
	Add that at the top of your code in a script tag
	<script>
		// in your code at top after loading createjs
		createjs.CSSPlugin.install();
		// the property must be set before you can animate
		zss("tagID").opacity = 1; // set this even if it is default
		animate(zid("tagID"), {opacity:0}, 2000); // etc.
	</script>
protect - (default false) protects animation from being interrupted before finishing
 	unless manually interrupted with stopAnimate()
	protect is always true (regardless of parameter setting) if loop or rewind parameters are set
override - (default true) subesequent tweens of any type on object cancel all earlier tweens on object
	set to false to allow multiple tweens of same object
from - |ZIM VEE| (default false) set to true to animate from obj properties to the current properties set on target
set - |ZIM VEE| (default null) an object of properties to set on the target to start (but after the wait time)
id - (default null) set to String to use with pauseAnimate(state, id) and stopAnimate(id) - thanks Sean Berwick for typo catch
	series animate gets only one overall id - so no id per animation object
events - (default false) set to true to receive an "animation" event on the target (or Container with a Container sequence)
sequenceTarget - (default null) used internally for processing sequence animations
dynamic - (default false) set to true to turn on dynamic speed animation via the percentSpeed property
	setting perecentSpeed (default 100) will adjust the speed of the animation
	to change speed with a Slider, Dial, MotionController, Accelerator, etc.
	use target.animate({props:{rotation:360}, dynamic:true, set:{percentSpeed:0}}); to start off with no animation
drag - (default false) used with path in props to drag along path
	This can be done while animating or while the animation is paused
	Setting drag to true will set startPaused to true as well - set startPaused to false to animate and drag
	With rewind set, drag lets you change the direction of an animation while animating
	To turn drag off use the pause() method that animate() adds to the target
clamp - (default true) used with dynamic and non-looping - set to false to let time pass beyond animation start and end
startPaused - (default false - true if drag is true) Boolean - set to true to start the animation in the paused state
	Good for animating manually with the percentComplete property
clean - (default true) set to false to not delete animation ids etc. at end of animate()
	Could then use percentComplete to position tween and pauseTween(false) to start animating again
	Note... once tween has waited, percentComplete does not include wait period
	So clean cannot be used to restart an animation with a wait after the animation has waited
	Use the replayTween() method of the target to restart the latest animation on a target
obj - (depreciated) the old version of props - kept for backwards compatibility

PROPERTIES - zim.animate() adds the following properties to any object it animates:
	paused - read-only - when animating, paused is set to false
		When paused with pauseAnimate() then paused is set to true
		When stopped or before animation, paused is undefined or null
		There is only one paused property per object so pausing a specific id
		on an object with multiple animations will set paused to true
		even if other animations are still running.
		Pausing multiple objects should work fine.
	percentSpeed - get or set the percent of the animation speed
		100% is regular time, 50% is half as fast, 200% is twice as fast -100% is reverse normal speed, etc.
		use target.animate({props:{rotation:360}, dynamic:true, set:{percentSpeed:0}}); to start off with no animation
	percentComplete - get or set percent complete (0-100)
		this allows you to scrub through an animation with a Slider, Dial, MotionController, etc.
		should probably set startPaused parameter of animate() to true
		Setting percentComplete to 100 may cause the animation to start at the beginning
		but can set rewind:true and then set percentComplete to 50 - this will animate "backwards"
		Can use animation event and a test of percentComplete to stop animation at a percentage
		see  https://zimjs.com/explore/squigglepart.html

METHODS - see pauseAnimate() and stopAnimate() under the METHODS module
	Also - zim.animate() adds a pause(state, time) method to the target IF dynamic is set to true (or drag is true)
	This matches the pause() of Dynamo and Scroller and is used by Accelerator
	   state - (default true) true pauses and setting the state to false will unpause the dynamic animation
	   time - (default 0) time in milliseconds to slow the animation down if pausing or speed it up if unpausing
	resetTween() - sets the target back to the state at the start of the last animate() call
	replayTween() - resets and runs the last animate() on the target - calls the animate function with the same parameters as the last time
EVENTS - zim animate() will add an "animation" event to the target IF the events parameter is set to true (default is false)
 	or the "animation" event will be added to the Container for a sequence in a Container
	or the "animation" event is added to the targets of an animation series
	If dynamic is set to true, will dispatch a "pause" event when animate is paused - could be delayed but time passed in to pause()

RETURNS the target for chaining (or null if no target is provided and run on zim with series)
--*///+45
	zim.animate = function(target, props, time, ease, call, params, wait, waitedCall, waitedParams, loop, loopCount, loopWait, loopCall, loopParams, loopWaitCall, loopWaitParams, rewind, rewindWait, rewindCall, rewindParams, rewindWaitCall, rewindWaitParams, sequence, sequenceCall, sequenceParams, sequenceReverse, ticker, cjsProps, css, protect, override, from, set, id, events, sequenceTarget, dynamic, drag, clamp, startPaused, clean, obj) {
		var sig = "target, props, time, ease, call, params, wait, waitedCall, waitedParams, loop, loopCount, loopWait, loopCall, loopParams, loopWaitCall, loopWaitParams, rewind, rewindWait, rewindCall, rewindParams, rewindWaitCall, rewindWaitParams, sequence, sequenceCall, sequenceParams, sequenceReverse, ticker, cjsProps, css, protect, override, from, set, id, events, sequenceTarget, dynamic, drag, clamp, startPaused, clean, obj";
		var duo; if (duo = zob(zim.animate, arguments, sig)) return duo;
		z_d("45");

		if (zim.ANIMATE == false || (!window.zns && !ANIMATE)) return target;

		var startParams = Array.prototype.slice.call(arguments);

		if (zot(props) && !zot(obj)) props = obj; // conversion of 7.1 props parameter to pre 7.1 obj parameter
		obj = props;
		if (zot(obj)) {if (zon) {zog("animate() - need props");}; return target;}

		// zik supports passing array of options or an object with min, max, integer, negative properties and zik will pick or calculate a random value
		target = zim.Pick.choose(target); time = zim.Pick.choose(time); ease = zim.Pick.choose(ease); wait = zim.Pick.choose(wait); loopCount = zim.Pick.choose(loopCount); loopWait = zim.Pick.choose(loopWait); rewind = zim.Pick.choose(rewind); rewindWait = zim.Pick.choose(rewindWait); sequenceReverse = zim.Pick.choose(sequenceReverse); from = zim.Pick.choose(from); set = zim.Pick.choose(set);

		// CJSPROPS
		// convert loop and rewind properties into the legacy cjs props object
		var newProps = {override: ((!zot(target) && !zot(target.zimX) && !zot(obj.x)) || (!zot(target) && !zot(target.zimY) && !zot(obj.y)))};
		if (!zot(loop)) newProps.loop = loop;
		if (!zot(loopCount)) newProps.count = loopCount; // note prop is count
		if (!zot(loopWait)) newProps.loopWait = loopWait;
		if (!zot(loopCall)) newProps.loopCall = loopCall;
		if (!zot(loopWaitParams)) newProps.loopWaitParams = loopWaitParams;
		if (!zot(loopWaitCall)) newProps.loopWaitCall = loopWaitCall;
		if (!zot(loopParams)) newProps.loopParams = loopParams;
		if (!zot(rewind)) newProps.rewind = rewind;
		if (!zot(rewindWait)) newProps.rewindWait = rewindWait;
		if (!zot(rewindCall)) newProps.rewindCall = rewindCall;
		if (!zot(rewindParams)) newProps.rewindParams = rewindParams;
		if (!zot(rewindWaitCall)) newProps.rewindWaitCall = rewindWaitCall;
		if (!zot(rewindWaitParams)) newProps.rewindWaitParams = rewindWaitParams;
		if (!zot(props)) newProps = zim.merge(newProps, cjsProps); // cjsProps to overwrite
		cjsProps = newProps;

		// SEQUENCE HANDLING
		// handle multiple targets first if there is an array
		// this just recalls the animate function for each element delayed by the sequence parameter
		if (zot(sequence)) sequence = 0;
		var sequenceTarget;
		if (sequence > 0 && target.addChild) { // container with sequence so convert target to array
			var newTarget = [];
			for (var i=0; i<target.numChildren; i++) {
				newTarget.push(target.getChildAt(i));
			}
			sequenceTarget = target;
			target = newTarget;
		}
		if (target instanceof Array) {
			if (sequenceReverse) target.reverse();
			var currentTarget = 0;
			for (var i=0; i<target.length; i++) {
				-function () { // closure to store num (i) for timeout
					var num = i;
					if (from) {
						var val;
						target[num].zimObj = {};
						for (var prop in obj) {
							val = zim.Pick.choose(obj[prop]);
							target[num].zimObj[prop] = target[num][prop];
							target[num][prop] = val;
						}
					} else {
						target[num].zimObj = {};
						for (var prop in obj) {
							target[num].zimObj[prop] = zim.Pick.choose(obj[prop]);
						}
					}
					setTimeout(function() {
						var t =	target[currentTarget];
						currentTarget++;
						zim.animate(t, t.zimObj, time, ease, call, params, wait, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, ticker, zim.copy(cjsProps), css, protect, override, null, set, id, events, sequenceTarget, dynamic, drag, clamp, startPaused, clean, obj); // do not send from!
						if (num == target.length-1 && sequenceCall) {
							// calculate tween time
							var duration = ((time)?time:1000) + ((wait)?wait:0); // wait only happens at start - no longer each time
							if (cjsProps && cjsProps.rewind) {
								duration += ((time)?time:1000) + ((cjsProps.rewindWait)?cjsProps.rewindWait:0);
							}
							if (cjsProps && cjsProps.loop && cjsProps.loopWait) {
								duration += cjsProps.loopWait;
							}
							setTimeout(function(){
								sequenceCall(sequenceParams);
							}, duration);
						}
					}, sequence*i);
				}();
			}
			return sequenceTarget;
		}

		// DEFAULTS
		var t = time;
		if (zot(t)) t = 1000;
		if (zot(ease)) ease = "quadInOut";
		if (zot(wait)) wait = 0;
		if (zot(cjsProps)) cjsProps = {override: (!zot(target.zimX) && !zot(obj.x) || !zot(target.zimY) && !zot(obj.y))};
		if (zot(params)) params = target;
		if (zot(ticker)) ticker = true;
		if (zot(css)) css = false;
		if (zot(protect)) protect = false;
		if (zot(from)) from = false;
		if (zot(set)) set = {};
		if (set.scale) {set.scaleX = set.scaleY = set.scale; delete set.scale}
		if (!zot(override)) cjsProps.override = override;
		if (zot(drag)) drag = false;
		if (zot(startPaused)) startPaused = drag?true:false;
		if (zot(dynamic)) dynamic = false;
		if (zot(clamp)) clamp = true;
		if (zot(clean)) clean = true;
		var tween;
		var idSet;
		var providedID;

		// ANIMATION SERIES HANDLING
		// if an array is passed in to animate() as the obj
		// then animate treats this as an animation series
		// [{target:circle, obj:{alpha:0}, time:1000}, {target:rect, obj:{alpha:0}, time:1000},]

		if (obj instanceof Array) {
			var starts;
			var currentCount = 1;
			if (obj.length == 0) return this;

			prepareSeries();
			prepareIds();
			runMaster();

			function runMaster() { // one day might consider reverse...
				var o; // inner obj
				var w = wait; // time for wait before starting animation
				var lastEnd = 0; // time of last label end
				var duration; // time of each label animation not including initial wait
				for (var i=0; i<obj.length; i++) {
					o = obj[i];
					if (zot(o.target)) continue;
					if (zot(o.time)) o.time = t;
					w += (o.wait?o.wait:0);
					duration = o.time;
					if (o.rewind) duration = duration * 2 + (o.rewindWait?o.rewindWait:0);
					if (o.loop) {
						// if loopCount is 0 (forever) then prepare series makes this the last animation
						duration *= o.loopCount;
						duration += (o.loopCount-1) * (o.loopWait?o.loopWait:0);
					}
					var currentObj = {
						target:o.target,
						obj:zim.copy(o.obj),
						wait:lastEnd+w,
						waitedCall:o.waitedCall,
						waitedParams:o.waitedParams,
						time:o.time,
						ease:o.ease,
						from:o.from,
						rewind:o.rewind,
						call:o.call,
						params:o.params,
						loop:o.loop, loopCount:o.loopCount, loopWait:o.loopWait,
						loopCall:o.loopCall, loopParams:o.loopParams,
						loopWaitCall:o.loopWaitCall, loopWaitParams:o.loopWaitParams,
						rewind:o.rewind, rewindWait:o.rewindWait,
						rewindCall:o.rewindCall, rewindParams:o.rewindParams,
						rewindWaitCall:o.rewindWaitCall, rewindWaitParams:o.rewindWaitParams,
						set:zim.copy(o.masterSet),
						events:events,
						override:false,
						id:id
					}
					if (i == obj.length-1) {
						endSeries(currentObj);
					}
					zim.animate(currentObj);
					lastEnd += w + duration;
					w = 0;
				}
			}
			function endSeries(currentObj) {
				var savedCall = currentObj.call;
				var savedParams = currentObj.params;
				if (cjsProps.loop && (!cjsProps.count || currentCount < cjsProps.count)) {
					currentObj.call = function() {
						if (cjsProps.loopCall && typeof cjsProps.loopCall == 'function') {(cjsProps.loopCall)(cjsProps.loopParams);}
						if (cjsProps.loopWait) {
							if (zot(target.zimTweens)) target.zimTweens = {};
							tween = target.zimTweens[id] = target.zimTween = createjs.Tween.get(target, {override:cjsProps.override}).wait(cjsProps.loopWait, true).call(goNext);
						} else {
							goNext();
						}
						function goNext() {
							for (var k=0; k<starts.objects.length; k++) {
								if (starts.objects[k].set) starts.objects[k].set(starts.values[k]);
							}
							if (cjsProps.loopWaitCall && typeof cjsProps.loopWaitCall == 'function') {(cjsProps.loopWaitCall)(cjsProps.loopWaitParams);}
							runMaster();
						}
					}
				} else {
					currentObj.call = function() {
						if (savedCall && typeof savedCall == 'function') {(savedCall)(savedParams);}
						if (call && typeof call == 'function') {(call)(params);}
						endTween(id);
					}
				}
				currentCount++;
			}
			function prepareSeries() {
				var froms = new zim.Dictionary();
				starts = new zim.Dictionary();
				for (var i=0; i<obj.length; i++) {
					o = obj[i];
					if (!target) target = o.target;

					if (zot(o.target)) o.target = target;
					if (zot(o.ease)) o.ease = ease;
					if (zot(o.target)) continue;
					if (zot(o.props) && !zot(o.obj)) o.props = o.obj;
					o.obj = o.props;
					if (o.loop && (zot(o.loopCount) || o.loopCount <= 0)) {
						o.loopCount = 0;
						// this object is looping forever so no point in keeping any next objects
						obj.splice(i+1, obj.length); // obj.length may be too much but it works
					}
					if (!zot(o.obj.scale)) {
						o.obj.scaleX = o.obj.scaleY = o.obj.scale;
						delete o.obj.scale;
					}
					if (o.from) {
						var firstFrom = froms.at(o.target);
						if (firstFrom) {
							if (o.set) {
								// all properties from obj go to set
								// matching firstFrom properties to to obj
								// matching set properties override firstFrom on obj
								var temp = zim.copy(o.obj);
								var merged = zim.merge(firstFrom, o.set);
								o.obj = getFroms(o.target, o.obj, merged);
								o.set = zim.merge(o.set, temp);
							} else {
								o.set = zim.copy(o.obj);
								o.obj = getFroms(o.target, o.obj, firstFrom);
							}
							o.from = false;
						} else {
							// any set properties override target properties
							froms.add(o.target, getFroms(o.target, o.obj, o.set));
						}
					}
					var startProps = {};
					for (var iii in o.obj) {
						startProps[iii] = o.set?(o.set[iii]?o.set[iii]:o.target[iii]):o.target[iii];
					}
					if (zot(starts.at(o.target))) starts.add(o.target, {});
					var newEntry = zim.merge(starts.at(o.target), startProps);
					starts.remove(o.target);
					starts.add(o.target, newEntry);
					o.masterSet = zim.copy(o.set);
				}
				if (zot(target.zimTweens)) target.zimTweens = {};
			} // end prepareSeries
			return target;
		} // end series

		if (obj) {
			if (obj.x) target.zimX = true;
			if (obj.y) target.zimY = true;
		}


		// SHAPE TWEEN
		if (!zot(obj.shape) && obj.shape.type == target.type && (target.type=="Blob" || target.type == "Squiggle")) {
			var targetPoints = target.pointObjects;
			var shapePoints = obj.shape.pointObjects;
			if (targetPoints.length != shapePoints.length) {
				if (zon) zog("ZIM animate() - shapes must be of same number of points");
				delete obj.shape;
			} else {
				var shape = obj.shape;
				delete obj.shape;
				var blobShift = zot(obj.blobShift)?0:obj.blobShift;
				delete obj.blobShift;
				var first = true;
				for (var i=0; i<targetPoints.length; i++) {
					var targetPoint = targetPoints[i];
					var shapePoint = shapePoints[(targetPoints.length*20000+i+blobShift)%targetPoints.length];
					// loop through the first four elements of the points array
					// the first element is the point container
					// the second is the circle inside (we leave this alone)
					// the third and forth are the two control rectangles
					for (j=0; j<4; j++) {
						if (j==1) continue;
						var controls = targetPoint[j];
						var shapeControls = shapePoint[j];
						// animate the control to the location of shapePoint
						// this will animate the whole control container when j=0
						// and the two rectangle control handles when j=2 and j=3
						var animateParams = zim.copy(startParams);
						animateParams[0] = controls;
						animateParams[1] = {x:shapePoint[j].x, y:shapePoint[j].y, rotation:shapePoint[j].rotation};
						// ~~~~~~~~~~~~~~
						// this needs to be changed if parameter order changes!
						// ~~~~~~~~~~~~~~
						if (first) {
							animateParams[34] = true; // events
							if (controls.animateEvent) controls.off("animation", controls.animateEvent);
							controls.animateEvent = controls.on("animation", function () {
								target.update();
							});
						}
						first = false;
						zim.animate.apply(zim.animate,animateParams);
					}
				}
				if (zim.isEmpty(obj)) return target;
			}
		}



		// -----------------------------
		// NORMALIZED TWEEN COMING THROUGH

		if (zot(target)) return;

		function isGetter (obj, prop) {
		  return !!Object.getOwnPropertyDescriptor(obj, prop)['get'];
		}

		if (target.type == "Sprite" || !target.hasOwnProperty("paused")) {
			Object.defineProperty(target, 'paused', {
				get: function() {
					return target.animatePaused;
				},
				set: function(value) {
					target.animatePaused = value;
				}
			});
		}

		// FOR RESET AND START of TWEEN
		target.zimTweenOriginals = {}
		zim.loop(obj, function (o) {
			target.zimTweenOriginals[o] = target[o];
		});
		target.resetTween = function() {
			zim.loop(target.zimTweenOriginals, function (o) {
				target[o] = target.zimTweenOriginals[o];
			});
			if (target.stage) target.stage.update();
		};
		target.replayTween = function() {
			target.resetTween();
			zim.animate.apply(null, startParams);
		}


		if (obj.path || dynamic) {
			// can't risk turning percentComplete off when animation ends as only one per all animations
			// instead, evaluate how percentComplete is set when starting an animation
			// if there is a value and no getter setter or a value has been changed when not tweening
			// (this last is because the getter setter may have previously been set - once set, it is not removed)
			// then assumes percentComplete is desired else sets to 0 when starting an animation
			// the startPercentComplete is then examined once the animation starts
			var startPercentComplete;
			if (target.requestedPercentComplete) startPercentComplete = target.requestedPercentComplete;
			else if (target.percentComplete && !isGetter(target, "percentComplete")) startPercentComplete = target.percentComplete;
			// the below did not work as expected with the first value being assigned if true - weird - so made above
			// var startPercentComplete = target.requestedPercentComplete //|| (target.percentComplete && !isGetter(target, "percentComplete"))?target.percentComplete:null;
			target.requestedPercentComplete = null;
			if (zot(startPercentComplete)) startPercentComplete = 0;
			target.pathRatio = 0; // could be wrong setting this to 0
		}

		if (css) ticker = false;
		if (zot(target.zimTweens)) target.zimTweens = {};

		var stage;
		if (!css) {
			if (!target.stage) {
				if (zimDefaultFrame) stage = zimDefaultFrame.stage;
				else return;
			} else {
				stage = target.stage;
			}
		}

		if (!zot(obj.scale)) {
			obj.scaleX = obj.scaleY = zim.Pick.choose(obj.scale);
			delete obj.scale;
		}
		// convert color tween
		if (target.setColorRange && !zot(obj.color)) {
			var color = obj.color;
			delete obj.color;
			obj.colorRange = 1;
			target.setColorRange(target.color, color);
			target.colorRange = 0;
		}

		// PROTECT LOOPS AND REWINDS WITH BUSY
		// if protected or a loop or rewind is currently running for any of these properties
		// then remove the property from obj as it is currently busy
		for(var o in obj) {
			if (!target.zimBusy) break;
			if (target.zimBusy[o]) delete obj[o];
		}

		if (zim.isEmpty(obj)) return; // nothing left to animate
		if (target.type != "Sprite") target.paused = false;
		var lastMouseEnabled = target.mouseEnabled;
		function addZimBusy() {
			target.mouseEnabled = false;
			setTimeout(function() {
				if (!target.zimBusy) target.zimBusy = {};
				for(var o in obj) {
					target.zimBusy[o] = true;
				}
				target.mouseEnabled = lastMouseEnabled;
			}, 70);
		}
		if (protect || cjsProps.loop || cjsProps.rewind) addZimBusy();

		// IDS and IDSETS
		// this is for ids and idSets on this target
		// a single tween for an id does not get an idSet
		// a second tween for the same id gets an idSet
		// the original id is the id for the idSet
		prepareIds();
		function prepareIds() {
			if (zot(id)) {
				id = zim.makeID(10);
			} else {
				id = String(id);
				providedID = id;
			}
			if (zot(target.zimIdSets)) target.zimIdSets = {};
			if (!zot(target.zimIdSets[id])) { // already an idSet
				idSet = id;
				id = zim.makeID(10);
				target.zimIdSets[idSet].push(id);
			} else if (!zot(target.zimTweens[id])) { // not an idSet but already a tween so make an idSet
				idSet = id;
				id = zim.makeID(10);
				// var idSet = target.zimTweens[id].zimIdSet;
				target.zimIdSets[idSet] = [idSet]; // add original into set
				target.zimTweens[idSet].zimIdSet = idSet; // reference back to idSet
				target.zimIdSets[idSet].push(id); // push the second one in
			} // else nothing - id is not currently part of idSet
		}

		// PREPARE ZIK RANDOM VALUES PASSED IN AS ARRAY OR RAND OBJECT {min, max, integer, negative}
		var extraTypes = ["extra", "zoom", "speed", "layer", "fade"];
		var extraLookup = {zoom:target.type=="Pen"?"size":"scale", speed:"percentSpeed", layer:"layer", fade:"alpha"};
		for (var i in obj) {
			if (extraTypes.indexOf(i) >= 0) continue; // skip for extras
			obj[i] = zim.Pick.choose(obj[i]);
		}
		extraTypes.shift(); // take off extra for later
		for (i in set) set[i] = zim.Pick.choose(set[i]);
		for (i in cjsProps) {
			if (i=="waitedCall" || i=="waitedParams" || i=="loopCall" || i=="rewindCall" || i=="loopWaitCall" || i=="rewindWaitCall") continue;
			cjsProps[i] = zim.Pick.choose(cjsProps[i]);
		}

		// PREPARE RELATIVE VALUES PASSED IN AS STRINGS
		for (i in obj) {
			if (typeof obj[i] == "string") {
				obj[i] = target[i] + Number(obj[i].replace(/\s/g,""));
			}
		}
		for (i in set) {
			if (typeof set[i] == "string") {
				set[i] = target[i] + Number(set[i].replace(/\s/g,""));
			}
		}


		// start NIO updates 1
		// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

		// PERCENT COMPLETE SETUP
		if (target.getBounds && zot(target.percentCompleteCheck)) {
			target.percentCompleteCheck = true;
			Object.defineProperty(target, 'percentComplete', {
				get: function() {
					if (this.zimTween) {
						return this.zimTween.duration ? this.zimTween.position / this.zimTween.duration * 100 : 0;
					} else {
						return 0;
					}
				},
				set: function(value) {
					if (this.zimTween) {
						this.zimTween.startPaused = false;
						// if (!this.paused) this.pauseAnimate(true, id);
						if (!dynamic) {
							this.zimTween.setPosition(Math.round(value*this.zimTween.duration/100));
						} else {
							if (!zot(this.zimTween.currentTime)) {
								this.zimTween.currentTime = this.zimTween.currentTime - this.zimTween.position + value*this.zimTween.duration/100;
							}
						}
						handlePath();
					} else {
						// to handle being set prior to a subsequent animation
						this.requestedPercentComplete = value;
					}
				}
			});
		}

		// PATH ANIMATION SETUP
		var pathObject;
		if (!zot(obj.path) && obj.path.segmentPoints) {
			if (target.type == "Pen") target.infinite = true;
			target.zimOnPath = true;
			if (zot(target.pathRatio)) target.pathRatio = 0;
			if (zot(obj.orient)) obj.orient = obj.path;
			pathObject = obj.path;
			var dynamicPath = !pathObject.lockControls;
			delete obj.path;
			obj.pathRatio = 1; // this is what we are animating to
		}

		// PREPARE START VALUES - now that pathRatio is set
		if (from) obj = getFroms(target, obj, set, true);
		function getFroms(target, obj, set, update) {
			// animating based on pathRatio - but API is percentComplete
			// with from set, this causes problems - so add pathRatio equal to percentComplete to set
			if (pathObject && (!zot(target.percentComplete) || (set && !zot(set.percentComplete)))) {
				if (!set) set = {};
				if (zot(set.pathRatio)) set.pathRatio = !zot(set.percentComplete) ? set.percentComplete/100 : target.percentComplete/100;
			}
			var newObj = {};
			for (i in obj) {
				if (set && !zot(set[i])) {
					newObj[i] = set[i];
				} else {
					newObj[i] = target[i];
				}
				if (update) target[i] = obj[i];
			}
			return newObj;
		}

		var lastPercentComplete = target.percentComplete;
		var flipCheck = true;
		var mouseCheck = false;
		var activeCheck = true;
		var latestSegmentIndex = 0;

		var redirect=true;
		if (!zot(obj.redirect)) {
			redirect=obj.redirect;
			delete obj.redirect;
		}

		// CONTINUE PATH ANIMATION SETUP
		if (pathObject) {
			var segments;
			var percents;
			if (!dynamicPath) {
				segments = zim.copy(pathObject.segmentPoints);
				percents = zim.copy(pathObject.segmentRatios);
			}
			var newPoint = pathObject.getCurvePoint(target.pathRatio, percents, segments);

			if (target.parent && newPoint) {
				var locPoint = target.parent.globalToLocal(newPoint.x, newPoint.y);
				target.x = locPoint.x;
				target.y = locPoint.y;
			}
			// DRAG SETUP
			// Fairly complex system - to implement damping on drag
			// dragging can be done when animation is on or off
			// dragging with rewind turned on lets you drag and release to redirect animation
			// dragging with animation off lets you throw the animation
			if (drag) {
				ease = "linear";
				target.cursor = "pointer";
				var pathPercent = target.percentComplete;
				var lastPause = startPaused?startPaused:target.paused;
				var lastForward = true;
				var currentForward = true;
				var lastPathPercent = pathPercent;
				var stage;

				var pixels = 10;
				var segments = copy(pathObject.segmentPoints);
			    var length = 0;
			    zim.loop(segments, function (point) {
			        length+=distanceAlongCurve(point);
			    });
			    var numPoints = Math.round(length/segments.length/pixels);
			    var testPoints = pathObject.interpolate(numPoints, null, true);
			    var currentDist = (pathPercent?pathPercent:0)/100*length;
			    var targetDist = currentDist;
				var dirTest = 0;
			    var changed = false;
			    var segmentRatios = zim.copy(pathObject.segmentRatios);
			    target.zimAnimatePathChange = pathObject.on("change", function() {changed = true;});

				target.zimAnimateDragDown = target.on("mousedown", function (e) {
					stage = e.target.stage;
					stage.mouseMoveOutside = true;
					flipCheck = false; // sometimes picking up target changes its direction - not good
					activeCheck = true;
					setTimeout(function() {
						flipCheck = true;
					}, 50);
					mouseCheck = true;

					if (changed) {
			            segments = zim.copy(pathObject.segmentPoints);
			            length = 0;
			            zim.loop(segments, function (point) {
			                length+=distanceAlongCurve(point);
			            });
						numPoints = Math.round(length/segments.length/pixels);
			            testPoints = pathObject.interpolate(numPoints, null, true);
			            segmentRatios = zim.copy(pathObject.segmentRatios);
			        }
			        changed = false;

					pathPercent = target.percentComplete;
					dampPercent.immediate(pathPercent);
					var calcPercent = dirTest = pathPercent;
					if (rewind) { // handle rewind
						if (tween.position > tween.duration/2) calcPercent = (100-pathPercent)*2;
						else calcPercent = pathPercent*2;
					}
					currentDist = (calcPercent?calcPercent:0)/100*length;
					targetDist = currentDist;
				})

				target.zimAnimateDragPress = target.on("pressmove", getPathPercent);
				function getPathPercent(e) {
					var point = pathObject.globalToLocal(e.rawX/stage.scaleX, e.rawY/stage.scaleY);
					var mousePlace = currentDist;
					// do not changes currentForward if no change in dirSamples
					if (pathObject == "Blob") {
						if (dirSamples[0] < dirSamples[4]) currentForward = true;
						else if (dirSamples[0] > dirSamples[4]) currentForward = false;
						if (mousePlace < 10 && !currentForward) {
							targetDist = currentDist = mousePlace = length-5;
							dampPercent.immediate(targetDist/length*100);
							dirSamples=[length-1,length-2,length,length-3,length-4];
						} else if (mousePlace > length-10 && currentForward) {
							targetDist = currentDist = mousePlace = 5;
							dampPercent.immediate(targetDist/length*100);
							dirSamples=[0,0,0,0,0];
						}
					}
					// thanks https://codepen.io/eliCrow/pen/MLdddR
				    var dPoint = {x:point.x, y:point.y, r:mousePlace};
			        testPoints.sort(function (a, b) {
			          var aPoint = {x:a.x, y:a.y, r:a.r*length};
			          var bPoint = {x:b.x, y:b.y, r:b.r*length};
			          var aDiff = Math.sqrt(Math.pow(dPoint.x-aPoint.x, 2)+Math.pow(dPoint.y-aPoint.y, 2)+Math.pow(dPoint.r-aPoint.r, 2));
					  var bDiff = Math.sqrt(Math.pow(dPoint.x-bPoint.x, 2)+Math.pow(dPoint.y-bPoint.y, 2)+Math.pow(dPoint.r-bPoint.r, 2));
					  return aDiff-bDiff;
			        })
					targetDist = testPoints[0].r*length;
					pathPercent = targetDist/length*100;
					pathPercent = constrain(pathPercent,0,99.95); // get rid of end glitch
					if (rewind) { // handle rewind
						pathPercent/=2;
						if (tween.position > tween.duration/2) pathPercent = 100-pathPercent;
					}
				}

				target.zimAnimateDragUp = target.on("pressup", function () {
					if (target.paused==false) mouseCheck = false;
					flipCheck = false;
					setTimeout(function() {
						flipCheck = true;
					}, 50);
					// do not change currentForward if no change in dirSamples
					if (dirSamples[0] < dirSamples[4]) currentForward = true;
					else if (dirSamples[0] > dirSamples[4]) currentForward = false;
					if (!target.paused && rewind && redirect && currentForward != lastForward) {
						target.percentComplete = 100-target.percentComplete;
						lastForward = currentForward;
						// can't seem to get this to change direction when
						// dragging when paused and then unpausing direction
					}
				});

				var dampPercent = new zim.Damp(pathPercent, .15);
				var lastPercent = 0;
				var dirCount = 0;
				var dirSamples = [0,0,0,0,0];
				target.zimDragAnimateTicker = zim.Ticker.add(function () {
					if (!drag) return; // might be paused
					if (++dirCount%20) {
						dirSamples.push(targetDist);
						dirSamples.shift();
					}
					currentDist = targetDist;
					// this was still running (due to easing) when unpaused so set mouseCheck to false in pause() script to solve
					if (mouseCheck || (activeCheck && target.paused==true)) {
						if (pathObject.type == "Blob" && Math.abs(lastPercent-pathPercent)>(rewind?45:90)) {
							var newPercent = pathPercent
							dampPercent.immediate(newPercent);
						} else {
							var newPercent = dampPercent.convert(pathPercent);
						}
						if (Math.abs(lastPercent-newPercent)>.1) {
							target.percentComplete = newPercent;
							lastPercent = newPercent;
						} else {
							target.percentComplete = newPercent; // changed lastPercent to newPercent
							//if (!mouseCheck) {
								activeCheck = false;
							//}
						}
					} else {
						activeCheck = false; // otherwise goes to last drag after animation turned off
					}
				}, stage);
			}
		}

		// ORIENTATION SETUP
		var pathOrient;
		if (obj.orient) {
			if (obj.orient === true) {
				var oPoint = target.parent.localToGlobal(!zot(obj.x)?obj.x:target.x, !zot(obj.y)?obj.y:target.y);
				obj.orient = {x:oPoint.x, y:oPoint.y};
			}
			pathOrient = obj.orient;
			delete obj.orient;
			var startAngle = target.rotation;
			if (pathOrient != pathObject) {
				var orientPoint;
				if (pathOrient.parent) {
					orientPoint = pathOrient.parent.localToLocal(pathOrient.x, pathOrient.y, target.parent);
				} else {
					orientPoint = target.parent.globalToLocal(pathOrient.x, pathOrient.y);
				}
				target.rotation = zim.angle(target.x, target.y, orientPoint.x, orientPoint.y)+startAngle;
			}
		}

		// FLIP SETUP
		var pathFlip;
		var startFlip;
		if (obj.flip) {
			pathFlip = obj.flip;
			delete obj.flip;
			startFlip = target.scaleX;
		}
		var pathFlipVertical;
		var startFlipVertical;
		if (obj.flipVertical) {
			pathFlipVertical = obj.flipVertical;
			delete obj.flipVertical;
			startFlipVertical = target.scaleY;
		}

		var lastX=target.x;
		var lastY=target.y;

		// end NIO updates
		// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


		// LOOP AND REWIND SETUP
		var count = 0;
		if (cjsProps.loop) {
			if (!zot(cjsProps.count)) {
				count = cjsProps.count;
				delete cjsProps.count;
				var currentCount = 1;
			}
		}
		var wait3 = 0;
		if (cjsProps.loopWait) {
			wait3 = cjsProps.loopWait;
			delete cjsProps.loopWait;
		}
		var call3;
		if (cjsProps.loopCall) {
			call3 = cjsProps.loopCall;
			delete cjsProps.loopCall;
		}
		var params3 = target;
		if (cjsProps.loopParams) {
			params3 = cjsProps.loopParams;
			delete cjsProps.loopParams;
		}
		function doLoopCall() {
			if (call3 && typeof call3 == 'function') {(call3)(params3);}
		}
		var call4;
		if (cjsProps.loopWaitCall) {
			call4 = cjsProps.loopWaitCall;
			delete cjsProps.loopWaitCall;
		}
		var params4 = target;
		if (cjsProps.loopWaitParams) {
			params4 = cjsProps.loopWaitParams;
			delete cjsProps.loopWaitParams;
		}
		function doLoopWaitCall() {
			if (call4 && typeof call4 == 'function') {(call4)(params4);}
		}

		// TWEENS FOR REWIND, LOOP and NORMAL
		if (cjsProps.rewind) {

			// flip second ease
			if (ease) {
				// backIn backOut backInOut
				var ease2 = ease;
				if (ease2.indexOf("InOut") == -1) {
					if (ease2.indexOf("Out") != -1) {
						ease2 = ease2.replace("Out", "In");
					} else if (ease2.indexOf("In") != -1) {
						ease2 = ease2.replace("In", "Out");
					}
				}
			}
			var wait2 = 0;
			delete cjsProps.rewind;
			if (cjsProps.rewindWait) {
				wait2 = cjsProps.rewindWait;
				delete cjsProps.rewindWait; // not a createjs prop so delete
			}

			var call2;
			if (cjsProps.rewindCall) {
				call2 = cjsProps.rewindCall;
				var params2 = cjsProps.rewindParams;
				if (zot(params2)) params2 = target;
				delete cjsProps.rewindCall;
				delete cjsProps.rewindParams;
			}
			function doRewindCall() {
				if (call2 && typeof call2 == 'function') {(call2)(params2);}
			}
			var call5;
			if (cjsProps.rewindWaitCall) {
				call5 = cjsProps.rewindWaitCall;
				var params5 = cjsProps.rewindWaitParams;
				if (zot(params5)) params5 = target;
				delete cjsProps.rewindWaitCall;
				delete cjsProps.rewindWaitParams;
			}
			function doRewindWaitCall() {
				if (call5 && typeof call5 == 'function') {(call5)(params5);}
			}

			if (wait > 0) { // do not want wait as part of future loops (use loopWait)
				tween = target.zimTweens[id] = target.zimTween = createjs.Tween.get(target, {override:cjsProps.override}).wait(wait, true).call(function(){
					if (waitedCall && typeof waitedCall == 'function') {(waitedCall)(zot(waitedParams)?target:waitedParams);}
					tween1(tween);
				});
			} else {
				tween1();
			}
			function tween1(lastTween) {
				var obj2 = getStart();
				if (target.set && !from) target.set(set);
				tween = target.zimTweens[id] =  target.zimTween = createjs.Tween.get(target, cjsProps)
					.to(obj, t, createjs.Ease[ease])
					.call(doRewindWaitCall)
					.wait(wait2, true)
					.call(doRewindCall)
					.to(obj2, t, createjs.Ease[ease2])
					.call(doneAnimating)
					.wait(wait3, true)
					.call(doLoopCall);
				if (lastTween) {
					tween.zimIdSet = lastTween.zimIdSet; // know we need this one - not sure about the rest 10.4.0
					tween.zimObj = lastTween.zimObj;
					tween.zimTicker = lastTween.zimTicker;
					tween.zimExtraTickers = lastTween.zimExtraTickers;
					tween.requestID = lastTween.requestID;
				}
				setZimTweenProps();
			}

		} else {

			if (wait > 0) { // do not want wait as part of future loops (use loopWait)
				tween = target.zimTweens[id] = target.zimTween = createjs.Tween.get(target, {override:cjsProps.override}).wait(wait, true).call(function(){
					if (waitedCall && typeof waitedCall == 'function') {(waitedCall)(zot(waitedParams)?target:waitedParams);}
					tween2(tween);
				});
			} else {
				tween2();
			}
			function tween2(lastTween) {
				if (target.set && !from) {target.set(set);}
				tween = target.zimTweens[id] =  target.zimTween = createjs.Tween.get(target, cjsProps)
					.to(obj, t, createjs.Ease[ease])
					.call(doneAnimating)
					.wait(wait3, true)
					.call(doLoopCall);
				if (lastTween) {
					tween.zimIdSet = lastTween.zimIdSet; // know we need this one - not sure about the rest 10.4.0
					tween.zimObj = lastTween.zimObj;
					tween.zimTicker = lastTween.zimTicker;
					tween.zimExtraTickers = lastTween.zimExtraTickers;
					tween.requestID = lastTween.requestID;
				}
				setZimTweenProps();
			}
		}
		tween.startPaused = startPaused; // turned off by pause(false) or pauseAnimate(false) or setting percentComplete
		tween.rewinding = false;

		// SET TICKER
		var zimTicker;
		if (!css && ticker) {
			// moved this to after Tween has started so rewind is taken into account, etc.
			// do not want to reset percentComplete at the end incase dragging
			// so just reset it to 0 if at 100
			// means to purposely start at end, must set rewind true and start at 50% and animate to 100% for instance
			if (startPercentComplete) {
				target.pathRatio = (target.percentComplete && Math.round(target.percentComplete) != 100)?target.percentComplete/100:0;
				target.percentComplete = Math.round(startPercentComplete) != 100?startPercentComplete:0;
			}
			if (events && !zot(sequenceTarget) && sequenceTarget.dispatchEvent) {
				zimTicker = zim.Ticker.add(function(){
					if (pathObject || pathOrient || pathFlip || pathFlipVertical) {handlePath()};
					sequenceTarget.dispatchEvent("animation");
				}, stage);
			} else if (events && target.dispatchEvent) {
				zimTicker = zim.Ticker.add(function(){
					if (pathObject || pathOrient || pathFlip || pathFlipVertical) {handlePath()};
					target.dispatchEvent("animation");
				}, stage);
			} else {
				zimTicker = zim.Ticker.add(function(){
					if (pathObject || pathOrient || pathFlip || pathFlipVertical) {handlePath()};
				}, stage);
			}
		}

		function totalArr(arr) {
		  if(!Array.isArray(arr)) return;
		  var totalNumber = 0;
		  for (var i=0,l=arr.length; i<l; i++) {
			 totalNumber+=arr[i];
		  }
		  return totalNumber;
		}

		var forwardStack = [1,1,1];

		function handlePath() {
			if (!target.parent || tween.passive || tween.startPaused) return;
			if (drag && rewind && ((lastPathPercent < 50 && target.percentComplete > 50) || (lastPathPercent > 50 && target.percentComplete < 50))) {
				lastForward = target.percentComplete<50;
			}
			lastPathPercent = target.percentComplete;
			if (pathObject) {
				if (dynamicPath) {
					segments = pathObject.segmentPoints;
					percents = pathObject.segmentRatios;
				}
				var newPoint = pathObject.getCurvePoint(target.pathRatio, percents, segments);
				if (newPoint) {
					latestSegmentIndex = newPoint.z; // point on path before the calculated point - used with drag
					var locPoint = target.parent.globalToLocal(newPoint.x, newPoint.y);
					target.x = locPoint.x;
					target.y = locPoint.y;
				}
			}
			if (pathFlip && flipCheck) {
				if (lastX > target.x && zim.sign(target.scaleX) == zim.sign(startFlip)) target.scaleX*=-1;
				if (lastX < target.x && zim.sign(target.scaleX) != zim.sign(startFlip)) target.scaleX*=-1;
			}
			if (pathFlipVertical && flipCheck) {
				if (lastY > target.y && zim.sign(target.scaleY) == zim.sign(startFlipVertical)) target.scaleY*=-1;
				if (lastY < target.y && zim.sign(target.scaleY) != zim.sign(startFlipVertical)) target.scaleY*=-1;
			}
			var aa;
			if (pathOrient) {
				if (pathObject==pathOrient) {
					aa = zim.angle(lastX, lastY, target.x, target.y)+startAngle;
				} else {
					var orientPoint;
					if (pathOrient.parent) {
						orientPoint = pathOrient.parent.localToLocal(pathOrient.x, pathOrient.y, target.parent);
					} else {
						orientPoint = target.parent.globalToLocal(pathOrient.x, pathOrient.y);
					}
					aa = zim.angle(target.x, target.y, orientPoint.x, orientPoint.y)+startAngle;
				}
				if (aa != 0) {
					if (pathFlip && lastX-target.x > 0) aa += 180;
					target.rotation = aa==startAngle?target.rotation:aa;
				}
			}

			// picking up target can sway results so take average of last three
			// could possibly use this with flip to make it less jittery when dropping

			if (drag) {
				var diff = target.x-lastX + target.y-lastY;
				if (Math.abs(diff) > 2) {
					forwardStack.push(sign(diff));
					forwardStack.shift();
					// tween.forward = forwardStack.reduce((a,b) => a+b, 0) > 0;
					 tween.forward = totalArr(forwardStack);
				}
			}

			lastX = target.x;
			lastY = target.y;
		}

		// SET MASK TO DYNAMIC
		if (!zot(target.zimMaskDynamic)) target.zimMaskApply(); // set mask set by zimMask to dynamic


		// ANIMATION DONE AND HELPER FUNCTIONS
		function doneAnimating() {
			if (target.paused && target.type != "Sprite") {
				if (call && typeof call == 'function') {(call)(params);}
				return;
			}
			if (cjsProps.loop) {
				if (count > 0) {
					if (currentCount < count) {
						doLoopWaitCall();
						currentCount++;
						return;
					} else {
						if (rewind) {
							if (target.set) target.set(getStart());
						} else {
							if (target.set) target.set(obj);
						}
					}
				} else {
					doLoopWaitCall();
					return;
				}
			}
			if (clean) endTween(id);
			else target.pauseAnimate(true, id);
			// target.animatePaused = null;
			// target.paused = true;
			if (call && typeof call == 'function') {(call)(params);}
		}
		function getStart() {
			// for rewind, we need to know the start value
			// which could be modified by the set parameter
			var startObj = {}
			for (var i in obj) {
				if (css) {
					if (!zot(set[i]) && !from) {
						startObj[i] = set[i];
					} else {
						startObj[i] = target.style[i];
					}
				} else {
					if (!zot(set[i]) && !from) {
						startObj[i] = set[i];
					} else {
						startObj[i] = target[i];
					}
				}
			}
			return startObj
		}
		function removeBusy(obj) {
			if (!target.zimBusy) return;
			for (var o in obj) {
				delete target.zimBusy[o];
			}
			if (zim.isEmpty(target.zimBusy)) target.zimBusy = null;
		}

		// PAUSE AND STOP MANAGEMENT
		var zimPaused = false;
		setZimTweenProps();
		function setZimTweenProps() {
			// used to keep track of tweens for various ids
			// for pauseAnimate() and stopAnimate() down below
			tween.zimObj = obj;
			tween.zimTicker = zimTicker;
			tween.zimPaused = zimPaused;
			if (idSet) tween.zimIdSet = idSet;
			if (providedID) {
				// add to zim.idSets for global animation pause and stop by id
				// zim.idSets = {id:[target, target], id:[target, target, target]}
				// watchout - global idSet works on provided IDS
				// local idSets work on multiple ids that are the same
				// so an object with one tween under a provided id is not locally an idSet
				if (zot(zim.idSets)) zim.idSets = {};
				if (zot(zim.idSets[providedID])) {
					zim.idSets[providedID] = [target];
				} else {
					if (zim.idSets[providedID].indexOf(target) < 0) zim.idSets[providedID].push(target); // ES6 needed
				}
			}
			if (!zim.animatedObjects) zim.animatedObjects = new zim.Dictionary(true);
			zim.animatedObjects.add(target, true);
		}
		function endTween(id) {
			if (zot(target.zimTweens) || zot(target.zimTweens[id])) return;
			target.zimX = null;
			target.zimY = null;
			removeBusy(target.zimTweens[id].zimObj);
			target.zimTweens[id].paused = true;
			endTicker(id);
			var idSet = target.zimTweens[id].zimIdSet;
			if (!zot(idSet) && target.zimIdSets) {
				var sets = target.zimIdSets[idSet];
				if (sets) sets.splice(sets.indexOf(id), 1);
				if (sets && sets.length == 0) {
					delete target.zimIdSets[idSet];
					if (zim.isEmpty(target.zimIdSets)) delete target.zimIdSets;
				}
			}
			//target.pathRatio = null;
			if (target.type == "Pen" && target.zimOnPath) {target.stop(); target.zimOnPath = false;}
			delete target.zimTweens[id];
			if (zim.isEmpty(target.zimTweens)) target.stopAnimate(null,null,false,true); // last true is internal


			// handle zim.idSets
			// very tricky - the originating id for an idSet does not get an idSet
			// but rather its id is used by subsequent tweens for the tween.idSets
			// the originating id may create a zim.idSets if it was provided as a parameter
			if ((target.zimTweens && target.zimTweens[id]) ||
				(target.zimIdSets && target.zimIdSets[idSet?idSet:id])) {
					// leave zim.idSets alone
			} else {
				if (zim.idSets && zim.idSets[idSet?idSet:id]) {
					zim.idSets[idSet?idSet:id]
					var targetIndex = zim.idSets[idSet?idSet:id].indexOf(target);
					if (targetIndex >= 0)  zim.idSets[idSet?idSet:id].splice(targetIndex, 1);
					if (zim.idSets[idSet?idSet:id].length <= 0) {
						delete zim.idSets[idSet?idSet:id];
						if (zim.isEmpty(zim.idSets)) delete zim.idSets;
					}
				}
			}
		}
		function endTicker(id) {
			// need a little delay to make sure updates the last animation
			// and to help call function have a stage update
			// store reference to ticker function in a closure
			// as we may delete the zimTweens reference before the 200 ms is up
			-function() {
				var ticker = target.zimTweens[id].zimTicker;
				if (target.zimTweens[id].extraTickers) {
					for (var k=0; k<target.zimTweens[id].extraTickers.length; k++) {
						zim.Ticker.remove(target.zimTweens[id].extraTickers[k]);
					}
				}
				if (ticker) {
					setTimeout(function(){
						if (ticker) zim.Ticker.remove(ticker); ticker = null;
					},200);
				}
			}();
		}
		function pauseTicker(id, paused) {
			var tween = target.zimTweens[id];
			tween.paused = paused;
			if (paused == tween.zimPaused) return;
			tween.zimPaused = paused;
			if (paused) {
				if (tween.zimTicker) tween.zimAnimateTimeout = setTimeout(function(){zim.Ticker.remove(tween.zimTicker);},200);
				// if (target.zimZoomTicker) zim.Ticker.remove(target.zimZoomTicker);
			} else {
				tween.startPaused = false;
				clearTimeout(tween.zimAnimateTimeout);
				if (tween.zimTicker) tween.zimTicker = zim.Ticker.add(tween.zimTicker, stage);
				// if (target.zimZoomTicker) target.zimZoomTicker = zim.Ticker.add(target.zimZoomTicker, stage);
			}
		}
		function expandIds(ids) {
			// turn any idSets into ids
			var actualIds = [];
			for (var i=0; i<ids.length; i++) {
				if (target.zimIdSets && !zot(target.zimIdSets[ids[i]])) {
					actualIds = actualIds.concat(target.zimIdSets[ids[i]]);
				} else {
					actualIds.push(ids[i]);
				}
			}
			return actualIds;
		}

		// METHODS ADDED TO TARGET
		if (!target.stopAnimate || !target.stopAnimate.real) { // empty method gets added by default
	        target.stopAnimate = function(ids, include, command, internal) {
				// can't risk turning percentComplete off as only one per all animations
				// instead, evaluate how percentComplete is set when starting an animation
				// if there is no getter setter or a value has been changed when not tweening
				// then assumes percentComplete is desired else sets to 0 when starting an animation
				// if (internal) target.percentComplete = target.pathRatio = 0;
				if (zot(command)) command = true;
				target.paused = true;
				if (target.type == "Pen" && target.zimOnPath) {target.stop(); target.zimOnPath = false;}
				if (zot(include)) include = true;
				if (command && drag) {
					if (pathObject) pathObject.off("change", target.zimAnimatePathChange);
					target.off("mousedown", target.zimAnimateDragDown);
		            target.off("pressmove", target.zimAnimateDragPress);
		            target.off("pressup", target.zimAnimateDragUp);
		            zim.Ticker.remove(target.zimDragAnimateTicker);
				}
				if (zot(ids)) {
					if (!include) return target; // would be exclude all ids
					target.zimBusy = null; // clear any busy properties
		            createjs.Tween.removeTweens(target);
					for (var id in target.zimTweens) {endTicker(id);}
					target.zimTweens = null;
					target.zimIdSets = null;
					target.zimTween = null;
					if (zim.idSets && zim.idSets[idSet?idSet:id]) {
						delete zim.idSets[idSet?idSet:id];
						if (zim.isEmpty(zim.idSets)) delete zim.idSets;
					}
					zim.animatedObjects.remove(target);
				} else {
					if (!Array.isArray(ids)) ids = [ids];
					// expand any idSets into ids
					var actualIds = expandIds(ids);
					for (var id in target.zimTweens) {
						if (target.zimTweens[id].requestID) {
							cancelAnimationFrame(target.zimTweens[id].requestID);
						}
						if (include && actualIds.indexOf(id) >= 0) endTween(id, command);
						if (!include && actualIds.indexOf(id) < 0) endTween(id, command);
					}
				}
				return target;
	        }
			target.stopAnimate.real = true; // record this as real method instead of empty method
	        target.pauseAnimate = function(paused, ids, include, ignoreDynamic) {
				if (target.type == "Pen") {
					if (paused === false) {
						if (target.zimOnPath) target.infinite = true;
					} else {
						target.stop();
					}
				}
				if (usingDynamic && !ignoreDynamic) {
					target.pause(paused, ids);
					return target;
				}
	            if (zot(paused)) paused = true;
				mouseCheck = false;
				target.paused = paused;
				if (zot(include)) include = true;
				if (zot(ids) && !include) return target; // would be exclude all ids
				if (zot(ids)) { // want all ids
					for (var id in target.zimTweens) {pauseTicker(id, paused);}
				} else {
					if (!Array.isArray(ids)) ids = [ids];
					// expand any idSets into ids
					var actualIds = expandIds(ids);
					for (var id in target.zimTweens) {
						if (include && actualIds.indexOf(id) >= 0) pauseTicker(id, paused);
						if (!include && actualIds.indexOf(id) < 0) pauseTicker(id, paused);
					}
				}
				return target;
	        }
		}

		// start NIO updates 2
		// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

		if (startPaused) target.pauseAnimate(true, id, null, true); // last true overrides dynamic check

		// EXTRA SETUP
		var extra;
		if (obj.extra) {
			extra = obj.extra;
			delete obj.extra;
			if (!Array.isArray(extra)) extra = [extra];
		}
		// handle incoming convenience properties:
		for (var i=0; i<extraTypes.length; i++) {
			var type = extraTypes[i];
			if (obj[type]) {
				if (zot(extra)) extra = [];
				makeExtra(type, obj[type]);
				delete obj[type];
			}
		}
		function makeExtra(prop, arr) {
			if (arr === true) extra.push({outputProp:extraLookup[prop]});
			else if (Array.isArray(arr)) extra.push({outputProp:extraLookup[prop], outputMin:arr[0], outputMax:arr[1], inputMin:arr[2], inputMax:arr[3]});
			else extra.push({outputProp:extraLookup[prop], outputMax:arr});
		}

		if (extra) {
			tween.extraTickers = [];
			var extraObj;
			var extraProportion;
			for (var i=0; i<extra.length; i++) {
				var eObj = extra[i];
				if (zot(eObj.inputObj)) eObj.inputObj = target;
				if (zot(eObj.inputProp)) eObj.inputProp = "y";
				if (zot(eObj.inputMin)) eObj.inputMin = 0;
				if (zot(eObj.inputMax)) eObj.inputMax = eObj.inputProp=="x"?stage.width:stage.height;
				if (zot(eObj.outputObj)) eObj.outputObj = target;
				if (zot(eObj.outputProp)) eObj.outputProp = "scale";
				if (zot(eObj.outputMin)) eObj.outputMin = 0;
				if (zot(eObj.outputMax)) eObj.outputMax = eObj.outputMin + 1;
				if (zot(eObj.constrain)) eObj.constrain = true;

				// ZIK all property values!
				for (var j in eObj) {
					obj[j] = zim.Pick.choose(obj[j]);
				}
				if (eObj.outputProp == "percentSpeed") dynamic = true;

				extraProportion = new zim.Proportion(
					eObj.inputMin, eObj.inputMax,
					eObj.outputMin, eObj.outputMax,
					eObj.factor, eObj.outputRound
				);
				var func = function() { // closure
					var eO = eObj;
					var eP = extraProportion;
					return function () {
						var newValue;
						var eTarget = eO.outputObj;
						if (eO.constrain) newValue = zim.constrain(eP.convert(eO.inputObj[eO.inputProp]), eO.outputMin, eO.outputMax);
						else newValue = eP.convert(eO.inputObj[eO.inputProp]);
						// various conveneince properties and exceptions
						if (eO.outputProp == "scale") {
							eTarget.scaleX = zim.sign(eTarget.scaleX)*newValue;
							eTarget.scaleY = zim.sign(eTarget.scaleY)*newValue;
						} else if (eO.outputProp == "scaleX") {
							eTarget.scaleX = zim.sign(eTarget.scaleX)*newValue;
						} else if (eO.outputProp == "scaleY") {
							eTarget.scaleY = zim.sign(eTarget.scaleY)*newValue;
						} else if (eO.outputProp == "layer") {
							if (!eTarget.parent) return;
							var newLayer = zim.constrain(Math.round(newValue),0,eTarget.parent.numChildren-1);
							if (newLayer != eTarget.parent.getChildIndex(eTarget)) eTarget.parent.setChildIndex(eTarget, newLayer);
						} else {
							eTarget[eO.outputProp] = newValue;
						}
					};
				}();
				func();
				tween.extraTickers.push(zim.Ticker.add(func, stage));
			}
		}

		// DYNAMIC ANIMATION
		var usingDynamic;
		var requestID;
		if (dynamic || target.zimAnimateDragDown) {
			if (dynamic) {
				usingDynamic = true;
				target.pauseAnimate(true, id, null, true); // last true overrides dynamic check
				target.paused = false;
			    tween.currentTime = 0;
			    var myTween = tween;
				if (zot(target.percentSpeed)) target.percentSpeed = tween.startPaused?0:100;
			    var baseSpeed = 1000/60;
			    tween.currentTime = 0;
			    var setToMiddle = (!wait && zot(loopCount) && loop);
				if (clamp && loop && zot(loopCount)) clamp = false;
				var calculatedDuration = time + (rewind?time:0) + (rewindWait?rewindWait:0) + (loopWait?loopWait:0);
				var clampMultiplier = 1;
				if (loop && loopCount) clampMultiplier = loopCount;
				var clampEnd;
				if (clamp) clampEnd = calculatedDuration*clampMultiplier;

			    function advanceTween() {
					tween.requestID = requestID = requestAnimationFrame(advanceTween);
					if (!dynamic || (tween.startPaused && target.percentSpeed==0)) return;
					tween.startPaused = false;

			        if (wait>0 && tween != myTween) {
			            wait = 0;
			            tween.currentTime = 0;
			            target.pauseAnimate(true, id);
						target.paused = false;
			            setToMiddle = (zot(loopCount) && loop);
			        }
			        if (setToMiddle) {
			            tween.currentTime += 10000 * calculatedDuration;
			            tween.setPosition(tween.currentTime, true);
			            setToMiddle = false;
			        }
					var newTime = tween.currentTime+baseSpeed*target.percentSpeed/100;
					if (clampEnd && wait==0) newTime = zim.constrain(newTime, 0, clampEnd);
					tween.currentTime = newTime;

			        tween.setPosition(tween.currentTime);
					if (pathObject || pathOrient || pathFlip || pathFlipVertical) {handlePath()};
			        stage.update();
			    }
			    advanceTween();
			}

			var lastSpeed;
			var pausing = false;
			target.pause = function(state, time) {
				if (zot(state)) state = true;
				if (zot(time)) time = 0;
				if (state) {
					if (target.zimAnimateDragDown) {
						drag = false;
						target.cursor = "default";
						if (pathObject) pathObject.off("change", target.zimAnimatePathChange);
						target.off("mousedown", target.zimAnimateDragDown);
						target.off("pressmove", target.zimAnimateDragPress);
						target.off("pressup", target.zimAnimateDragUp);
						if (!dynamic) return;
					}
					lastSpeed = target.percentSpeed;
					if (time > 0) {
						pausing = true;
						zim.animate({target:target, props:{percentSpeed:0}, override:false, ticker:false, time:time, call:function() {
							pausing = false;
							target.percentSpeed = 0;
							target.paused = true;
							dynamic = false;
							target.dispatchEvent("pause");
						}});
					} else {
						tween.startPaused = false;
						pausing = false;
						target.percentSpeed = 0;
						target.paused = true;
						dynamic = false;
						setTimeout(function() {target.dispatchEvent("pause");}, 10);
					}
				} else {
					if (target.zimAnimateDragDown) {
						drag = true;
						target.cursor = "pointer";
						if (pathObject) target.zimAnimatePathChange = pathObject.on("change", target.zimAnimatePathChange);
						target.zimAnimateDragDown = target.on("mousedown", target.zimAnimateDragDown);
						target.zimAnimateDragPress = target.on("pressmove", target.zimAnimateDragPress);
						target.zimAnimateDragUp = target.on("pressup", target.zimAnimateDragUp);
						if (!dynamic) return;
					}
					mouseCheck = false;
					tween.currentTime = tween.position;
					if (time > 0) {
						pausing = true;
						zim.animate({
							target:target,
							props:{percentSpeed:lastSpeed},
							override:false,
							ticker:false,
							time:time,
							call:function() {
								pausing = false;
								target.percentSpeed = lastSpeed;
								target.paused = false;
								dynamic = true;
							}
						});
					} else {
						pausing = false;
						target.percentSpeed = lastSpeed;
						target.paused = false;
						dynamic = true;
					}
				}
				return target;
			}
		}

		// end NIO updates
		// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

		return target;
	}//-45

/*--
obj.stopAnimate = function(ids)

stopAnimate
zim function - and Display object function

DESCRIPTION
Stops tweens with the passed in id or array of ids.
If no id is passed then this will stop all tweens.
The id is set as a animate, move, Sprite parameter
An animation series will have the same id for all the animations inside.
To stop a Shape tween, set an id in the animate call and use that id to stop the shape tween
this is because the shape tween animation is not on the shape but on its many control points
See also pauseAnimate

NOTE: formerly stopZimAnimate - which still works but is depreciated

NOTE: calling stopAnimate(id) stops tweens with this id on all objects
calling object.stopAnimate(id) stops tweens with this id on the target object

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// We have split the tween in two so we can control them individually
// Set an id parameter to stop or pause
// You can control multiple tweens at once by using the same id (the id is for a tween set)
// Note the override:true parameter
var rect = new Rectangle(200, 200, pink)
	.centerReg(stage)
	.animate({obj:{scale:2}, time:2000, loop:true, rewind:true, id:"scale"})
	.animate({obj:{rotation:360}, time:4000, loop:true, ease:"linear", override:false});
rect.cursor = "pointer";
rect.on("click", function() {rect.stopAnimate()}); // will stop all tweens on rect
// OR
rect.on("click", function() {rect.stopAnimate("scale");}); // will stop scaling tween

stopAnimate("scale") // will stop tweens with the scale id on all objects

stopAnimate(); // will stop all animations
END EXAMPLE

PARAMETERS
ids - (default null) pass in an id or an array of ids specified in animate, move and Sprite

PROPERTIES
paused - zim.stopAnimate() will set paused property of stopped objects to null

RETURNS null if run as stopAnimate() or the obj if run as obj.stopAnimate()
--*///+45.12
	zim.stopAnimate = function(ids) {
		z_d("45.12");
		if (zot(ids)) {
			if (zim.animatedObjects) {
				for (var i=zim.animatedObjects.length-1; i>=0; i--) {
					zim.animatedObjects.objects[i].stopAnimate();
				}
			}
			return;
		}
		if (!Array.isArray(ids)) ids = [ids];
		if (!zim.idSets) return;
		for (var j=0; j<ids.length; j++) {
			var idSet = ids[j];
			if (zim.idSets[idSet]) {
				var idLength = zim.idSets[idSet].length-1;
				for (var i=idLength; i>=0; i--) {
					zim.idSets[idSet][i].stopAnimate(idSet);
				}
			}
		}
	}//-45.12

/*--
obj.stopZimAnimate = function(ids)

stopZimAnimate
This is replaced by stopAnimate()
--*///+45.1
	zim.stopZimAnimate = function(ids) {
		z_d("45.1");
		zim.stopAnimate(ids);
	}//-45.1

/*--
obj.pauseAnimate = function(state, ids)

pauseAnimate
zim function - and Display object function

DESCRIPTION
Pauses or unpauses tweens with the passed in id or array of ids.
If no id is passed then this will pause or unpause all tweens.
The id is set as a animate, move, Sprite parameter.
An animation series will have the same id for all the animations inside.
To pause a Shape tween, set an id in the animate call and use that id to pause the shape tween
this is because the shape tween animation is not on the shape but on its many control points
See also stopAnimate

NOTE: formerly pauseZimAnimate - which still works but is depreciated

NOTE: calling pauseAnimate(true, id) pauses tweens with this id on all objects
calling object.pauseAnimate(true, id) pauses tweens with this id on the target object

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle().center().animate({obj:{alpha:0}, repeat:true, rewind:true});
circle.on("mousedown", function() {
	circle.pauseAnimate(!circle.paused);
});
END EXAMPLE

EXAMPLE
// We have split the tween in two so we can control them individually
// Set an id parameter to stop or pause
// You can control multiple tweens at once by using the same id (the id is for a tween set)
// note the override:true parameter
var rect = new Rectangle(200, 200, pink)
	.centerReg(stage)
	.animate({obj:{scale:2}, time:2000, loop:true, rewind:true, id:"scale"})
	.animate({obj:{rotation:360}, time:4000, loop:true, ease:"linear", override:false});
rect.cursor = "pointer";
rect.on("click", function() {rect.pauseAnimate()}); // will pause all tweens on rect
// OR
var paused = false;
rect.on("click", function() {
	paused = !paused;
	rect.pauseAnimate(paused, "scale");
}); // will toggle the pausing of the scaling tween

pauseAnimate(false, "scale") // will unpause tweens with the scale id on all objects

pauseAnimate(); // will pause all animations
END EXAMPLE

PARAMETERS
state - (default true) will pause tweens - set to false to unpause tweens
ids - (default null) pass in an id or an array of ids specified in animate, move and Sprite

PROPERTIES
paused - zim.stopAnimate() will set paused property of paused objects to the value of the state parameter

RETURNS null if run as pauseAnimate() or the obj if run as obj.pauseAnimate()
--*///+45.22
	zim.pauseAnimate = function(state, ids) {
		z_d("45.22");
		if (zot(state)) state = true;
		if (zot(ids)) {
			if (zim.animatedObjects) {
				for (var i=zim.animatedObjects.length-1; i>=0; i--) {
					zim.animatedObjects.objects[i].pauseAnimate(state);
				}
			}
			return;
		}
		if (!Array.isArray(ids)) ids = [ids];
		if (!zim.idSets) return;
		for (var j=0; j<ids.length; j++) {
			var idSet = ids[j];
			 if (zim.idSets[idSet]) {
				for (var i=zim.idSets[idSet].length-1; i>=0; i--) {
					zim.idSets[idSet][i].pauseAnimate(state, idSet);
				}
			}
		}
	}//-45.22

/*--
obj.pauseZimAnimate = function(state, ids)

pauseZimAnimate
This is replaced by pauseAnimate()
--*///+45.2
	zim.pauseZimAnimate = function(state, ids) {
		z_d("45.2");
		zim.pauseAnimate(state, ids);
	}//-45.2

/*--
obj.wiggle = function(property, baseAmount, minAmount, maxAmount, minTime, maxTime, totalTime, type, ease, integer, id, startType)

wiggle
zim DisplayObject method

DESCRIPTION
Wiggles the property of the target object between a min and max amount to either side of the base amount
in a time between the min and max time.
Uses animate() so to pause or stop the wiggle use pauseAnimate and stopAnimate
either on the object or using an id that you pass in as a parameter

NOTE: calling pauseAnimate(true, id) pauses tweens with this id on all objects
calling target.pauseAnimate(true, id) pauses tweens with this id on the target object

EXAMPLE
var ball = new Circle().centerReg(stage);
ball.wiggle("x", ball.x, 10, 30, 300, 1000);
// wiggles the ball 10-30 pixels to the left and right of center taking 300-1000 ms each time

ball.pauseAnimate(); // will pause the wiggle
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function

NOTE: if using wiggle as a zim function the first parameter is:
target - the object to wiggle

property - the property name as a String that will be width-indicatorLength-edgeLeft-edgeRight
baseAmount - |ZIM VEE| the center amount for the wiggle - wiggle will go to each side of this center
minAmount - |ZIM VEE| the min amount to change to a side of center
maxAmount - |ZIM VEE| (default minAmount) the max amount to change to a side of center
minTime - |ZIM VEE| (default 1000 ms) the min time in milliseconds to go from one side to the other
maxTime - |ZIM VEE| (default minTime) the max time in milliseconds to go from one side to the other
totalTime - (default forever) the total time in milliseconds until a stopAnimate is called on wiggle
	adds a wiggleTimeout property to the wiggle target that holds the setTimeout id for cancelation of totalTime
type - (default "both") set to "positive" to wiggle only the positive side of the base or "negative" for negative side (or "both" for both)
ease - (default "quadInOut") the ease to apply to the animation
integer - (default false) tween to an integer value between min and max amounts
id - (default random id) the id to use for pauseAnimate() or stopAnimate()
startType - (default "both") set to "positive" to start wiggle in the positive side of the base or "negative" for negative side (or "both" for either)
ticker - (default true) set to false if wiggling a value other than a Display object property

EVENTS
if totalTime is set, target will dispatch a wigglestop event when the wiggle stops

RETURNS target for chaining
--*///+45.25
	zim.wiggle = function(target, property, baseAmount, minAmount, maxAmount, minTime, maxTime, totalTime, type, ease, integer, id, startType, ticker) {
		var sig = "target, property, baseAmount, minAmount, maxAmount, minTime, maxTime, totalTime, type, ease, integer, id, startType, ticker";
		var duo; if (duo = zob(zim.wiggle, arguments, sig)) return duo;
		z_d("45.25");
		if (zot(target) || zot(baseAmount) || zot(minAmount)) return target;
		if (zot(maxAmount)) maxAmount = minAmount;
		if (zot(minTime)) minTime = 1000;
		if (zot(maxTime)) maxTime = minTime;
		if (zot(ease)) ease = "quadInOut";
		if (zot(integer)) integer = false;
		if (zot(id)) id = zim.makeID();
		if (zot(type)) type = "both";
		if (zot(startType)) startType = "both";
		if (zot(ticker)) ticker = true;


		if (!zot(totalTime)) target.wiggleTimeout = setTimeout(function() {
			target.stopAnimate(id);
			target.dispatchEvent("wigglestop");
		}, totalTime);

		var results = {};
		var count = 0;
		var lastWiggle;
		function wiggleMe() {
			var time = zim.rand(zim.Pick.choose(minTime), zim.Pick.choose(maxTime));
			var obj = {};
			var set = {};
			set[property] = zim.Pick.choose(baseAmount);
			// to start go from center
			if (type == "negative" || (count==0 && startType == "negative")) {
				var wiggle = - zim.rand(zim.Pick.choose(minAmount),zim.Pick.choose(maxAmount),integer);
			} else if (type == "positive" || (count==0 && startType == "positive")) {
				var wiggle = zim.rand(zim.Pick.choose(minAmount),zim.Pick.choose(maxAmount),integer);
			} else {
				if (count == 0) {
					var wiggle = zim.rand(zim.Pick.choose(minAmount),zim.Pick.choose(maxAmount),integer,true); // negative or positive
				} else {
					var wiggle = zim.rand(zim.Pick.choose(minAmount),zim.Pick.choose(maxAmount),integer)*zim.sign(lastWiggle)*-1;
				}
			}
			obj[property]=zim.Pick.choose(baseAmount)+wiggle;
			if (count == 0) time = time/2;
			lastWiggle = wiggle;
			count++;
			if (type == "negative" || type == "positive") {
				zim.animate({target:target, obj:obj, set:set, ease:ease, time:time*2, rewind:true, override:false, call:wiggleMe, id:id, ticker:(target.stage?true:false)});
			} else {
				zim.animate({target:target, obj:obj, ease:ease, time:time, override:false, call:wiggleMe, id:id, ticker:(target.stage?true:false)});
			}		}
		wiggleMe();
		return target;
	}//-45.25

/*--
obj.loop = function(call, reverse, step, start, end)

loop
zim DisplayObject method
NOTE: overrides earlier loop function with added container loop
so that we can use earlier loop function without createjs

DESCRIPTION
The loop function (see the CODE module for loop) lets you loop through:
1. If you pass in a Number for obj then loop() does function call that many times
2. If you pass in an Array then loop() loops through the array
3. If you pass in an Object literal then loop() loops through the object
4. If you pass in an HTML NodeList or HTMLCollection then loop() loops through the tags

The loop method lets you loop through a Container
NOTE: you can also pass the container as the first parameter of the loop function

5. The loop method loops through all the children of the container
and does the function for each one passing the child, currentIndex, totalLoops, startIndex, endIndex, obj.
So this is like for(i=0; i<obj; i++) {var child = obj.getChildAt(i);} loop
or for (var i in container.children) {var child = container.children[i];}

NOTE: If you pass in true for reverse, the loop is run backwards counting to 0 (by default)
NOTE: use return to act like a continue in a loop and go to the next loop
NOTE: return a value to return out of the loop completely like a break (and return a result if desired)


EXAMPLE
var container = new Container();
// LOOP FUNCTION - for more examples see the CODE Module loop() function
loop(1000, function(i) { // gets passed an index i, total 1000, start 0, end 999, obj 1000
	// make 1000 rectangles
	container.addChild(new Rectangle());
});
stage.addChild(container);

// LOOP METHOD
// loop through children of the container
container.loop(function(child, i) { // gets passed the child, index, total, start, end and obj
	child.x += i*2;
	child.y += i*2;
}, true); // true would reverse - so highest in stack to lowest, with i going from numChildren-1 to 0
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
call - the function to call
	the function will receive (as its final parameters) the index, total, start, end, obj
		where the index is the current index, total is how many times the loop will run
		start is the start index, end is the end index and obj is the object passed to the loop
	the starting parameters vary depending on the type of obj:
	if the obj is a number then the first parameter is the index (no extra starting parameters given)
	if the obj is an array then the first parameter is the element at the current index
	if the obj is an object literal then the first and second parameters are the property name and property value at the current index
	if the obj is a container then the first parameter is the child of the container at the current index
reverse - (default false) set to true to run the loop backwards to 0
step - (default 1) each step will increase by this amount (positive whole number - use reverse to go backwards)
start - (default 0 or length-1 for reverse) index to start
end - (default length-1 or 0 for reverse) index to end

RETURNS any value returned from the loop - or undefined if no value is returned from a loop
--*///+45.3
	zim.zimLoopCheck = false;
	zim.loop = function(obj, call, reverse, step, start, end) {
		var sig = "obj, call, reverse, step, start, end";
		var duo; if (duo = zob(zim.loop, arguments, sig)) return duo;
		if (!zim.zimLoopCheck) z_d("45.3");
		zim.zimLoopCheck = true;
		if (zot(obj) || zot(call)) return undefined;
		if (zot(reverse)) reverse = false;
		if (zot(step) || step <= 0) step = 1;

		var type = typeof obj=="number"?"number":(obj.constructor === Array?"array":(obj.constructor === {}.constructor?"object":(obj instanceof NodeList?"nodelist":(obj instanceof HTMLCollection?"htmlcollection":"invalid"))));

		if (type == "container" && !obj.addChild) {
			return undefined;
		}
		if (type == "number" || type == "array" || type == "nodelist" || type == "htmlcollection") {
			var length = type=="number"?obj:obj.length;
			var total = getTotal(length-1);
			if (total == 0) return;
			if (reverse) {
				for(var i=start; i>=end; i-=step) {
					if (type=="number") {
						var r = call(i, total, start, end, obj);
					} else if (type=="array") {
						var r = call(obj[i], i, total, start, end, obj);
					} else { // nodelist
						var r = call(obj.item(i), i, total, start, end, obj);
					}
					if (typeof r != 'undefined') return r;
				}
			} else {
				for(var i=start; i<=end; i+=step) {
					if (type=="number") {
						var r = call(i, total, start, end, obj);
					} else if (type=="array") {
						var r = call(obj[i], i, total, start, end, obj);
					} else { // nodelist // htmlcollection
						var r = call(obj.item(i), i, total, start, end, obj);
					}
					if (typeof r != 'undefined') return r;
				}
			}
		} else if (type == "object") {
			var length = 0;
			var props = [];
			for (var i in obj) {
				length++;
				props.push(i);
			}
			var total = getTotal(length-1);
			if (total == 0) return;
			if (reverse) {
				for(var i=start; i>=end; i-=step) {
					var r = call(props[i], obj[props[i]], i, total, start, end, obj);
					if (typeof r != 'undefined') return r;
				}
			} else {
				for(var i=start; i<=end; i+=step) {
					var r = call(props[i], obj[props[i]], i, total, start, end, obj);
					if (typeof r != 'undefined') return r;
				}
			}
		} else {
			var total = getTotal(obj.numChildren-1);
			if (total == 0) return;
			if (reverse) {
				for(var i=start; i>=end; i-=step) {
					var r = call(obj.getChildAt(i), i, total, start, end, obj);
					if (typeof r != 'undefined') return r;
				}
			} else {
				for(var i=start; i<=end; i+=step) {
					var r = call(obj.getChildAt(i), i, total, start, end, obj);
					if (typeof r != 'undefined') return r;
				}
			}
		}
		function getTotal(max) {
			if (zot(start)) start = reverse?max:0;
			if (zot(end)) end = reverse?0:max;
			if ((reverse && end > start) || (!reverse && start > end)) return 0;
			if ((start < 0 && end) <0 || (start > max && end > max)) return 0;
			start = Math.max(0, Math.min(start, max));
			end = Math.max(0, Math.min(end, max));
			return Math.floor((reverse?(start-end):(end-start)) / step) + 1;
		}
	}//-45.3


// SUBSECTION GENERAL

/*--
obj.scaleTo = function(boundObj, percentX, percentY, type, boundsOnly)

scaleTo
zim DisplayObject method

DESCRIPTION
Scales object to a percentage of another object's bounds and scale
Percentage is from 0 - 100 (not 0-1).
Also see sca(), fit() and Layout().

EXAMPLE
circle.scaleTo(stage, 50); // scale to half the stageW
circle.scaleTo(stage, 10, 20); // fit within these scalings of the stage
END EXAMPLE

PARAMETERS - supports DUO - parameters or single object with properties below
boundObj - the object that we are scaling to with percents below
percentX - (default no scaling) the scale in the x
percentY - (default no scaling) the scale in the y
	if both percentX and percentY are missing then assumes 100, 100 for each
type - (default "smallest") to fit inside or outside or stretch to bounds
	smallest: uses the smallest scaling keeping proportion (fit)
	biggest: uses the largest scaling keeping proportion (outside)
	both: keeps both x and y scales - may stretch object (stretch)
boundsOnly - (default false) set to true to scale to the boundObj's bounds only - ignoring current boundObj scale

RETURNS obj for chaining
--*///+43
	zim.scaleTo = function(obj, boundObj, percentX, percentY, type, boundsOnly) {
		var sig = "obj, boundObj, percentX, percentY, type, boundsOnly";
		var duo; if (duo = zob(zim.scaleTo, arguments, sig)) return duo;
		z_d("43");
		if (zot(obj) || !obj.getBounds || !obj.getBounds()) {zog ("zim methods - scaleTo(): please provide an object (with setBounds) to scale"); return;}
		if (zot(boundObj) || !boundObj.getBounds || !boundObj.getBounds()) {zog ("zim methods - scaleTo(): please provide a boundObject (with setBounds) to scale to"); return;}
		if (zot(percentX)) percentX = -1;
		if (zot(percentY)) percentY = -1;
		if (percentX == -1 && percentY == -1) percentX = percentY = 100;
		if (zot(type)) type = "smallest";
		if (zot(boundsOnly)) boundsOnly = false;
		var w = boundObj.getBounds().width * percentX / 100 * (boundsOnly?1:boundObj.scaleX);
		var h = boundObj.getBounds().height * percentY / 100 * (boundsOnly?1:boundObj.scaleY);
		if (boundObj.type=="Stage" || boundObj.type=="StageGL") {
			w = boundObj.width * percentX / 100;
			h = boundObj.height * percentY / 100;
		}
		if ((percentX == -1 || percentY == -1) && type != "both" && type != "stretch") {
			if (percentX == -1) {
				zim.sca(obj, h/obj.getBounds().height);
			} else {
				zim.sca(obj, w/obj.getBounds().width);
			}
			return obj;
		}
		if (type == "both" || type == "stretch") {
			obj.scaleX = (percentX != -1) ? w/obj.getBounds().width : obj.scaleX;
			obj.scaleY = (percentY != -1) ? h/obj.getBounds().height : obj.scaleY;
			return obj;
		} else if (type == "biggest" || type == "largest" || type == "outside") {
			var scale = Math.max(w/obj.getBounds().width, h/obj.getBounds().height);
		} else { // smallest or fit
			var scale = Math.min(w/obj.getBounds().width, h/obj.getBounds().height);
		}
		zim.sca(obj, scale);
		return obj;
	}//-43

/*--
obj.fit = function(left, top, width, height, inside)

fit
zim DisplayObject method

DESCRIPTION
Scale an object to fit inside (or outside) a rectangle and center it.
Actually scales and positions the object.
Object must have bounds set (setBounds()).

EXAMPLE
circle.fit(100, 100, 200, 300); // fits and centers in these dimensions
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
left, top, width, height - (default stage dimensions) the rectangle to fit
inside - (default true) fits the object inside the rectangle
	if inside is false then it fits the object around the bounds
	in both cases the object is centered

RETURNS an Object literal with the new and old details (bX is rectangle x, etc.):
{x:obj.x, y:obj.y, width:newW, height:newH, scale:scale, bX:left, bY:top, bWidth:width, bHeight:height}
--*///+46
	zim.fit = function(obj, left, top, width, height, inside) {
		var sig = "obj, left, top, width, height, inside";
		var duo; if (duo = zob(zim.fit, arguments, sig)) return duo;
		z_d("46");
		if (zot(obj) || !obj.getBounds) return;
		if (!obj.getBounds()) {
			zog("zim methods - fit(): please setBounds() on object");
			return;
		}
		if (zot(left)) {
			if (!obj.stage) {
				zog("zim methods - fit(): please add boundary dimensions or add obj to stage first");
				return;
			}
			if (!obj.stage.getBounds()) {
				zog("zim methods - fit(): please add boundary dimensions or add obj with bounds to stage first");
				return;
			}
			var stageW = obj.stage.getBounds().width;
			var stageH = obj.stage.getBounds().height;
			left = 0; top = 0;
			width = stageW; height = stageH;
		}
		if (zot(inside)) inside = true;

		obj.scaleX = obj.scaleY = 1;

		var w = width;
		var h = height;
		var objW = obj.getBounds().width;
		var objH = obj.getBounds().height;
		var scale;

		if (inside) { // fits dimensions inside screen
			if (w/h >= objW/objH) {
				scale = h / objH;
			} else {
				scale = w / objW;
			}
		} else { // fits dimensions outside screen
			if (w/h >= objW/objH) {
				scale = w / objW;
			} else {
				scale = h / objH;
			}
		}

		obj.scaleX = obj.scaleY = scale;

		var newW = objW * scale;
		var newH = objH * scale;

		// horizontal center
		obj.x = (obj.regX-obj.getBounds().x)*scale + left + (w-newW)/2;

		// vertical center
		obj.y = (obj.regY-obj.getBounds().y)*scale + top + (h-newH)/2;

		return {x:obj.x, y:obj.y, width:newW, height:newH, scale:scale, bX:left, bY:top, bWidth:width, bHeight:height};

	}//-46

/*--
obj.boundsToGlobal = function(rect, flip)

boundsToGlobal
zim DisplayObject method

DESCRIPTION
Returns a createjs Rectangle of the bounds of object projected onto the stage.
Handles scaling and rotation.
If a createjs rectangle is passed in then it converts this rectangle
from within the frame of the obj to a global rectangle.
If flip (default false) is set to true it goes from global to local rect.
Used by drag() and hitTestBounds() above - probably you will not use this directly.

EXAMPLE
zog(circle.boundsToGlobal().x); // global x of circle
END EXAMPLE

PARAMETERS
rect - (default null) a rect inside an object which you would like mapped to global
flip - (default false) make a global rect ported to local values

RETURNS a createjs Rectangle of the bounds of object projected onto the stage
--*///+40
	zim.boundsCheck = false;
	zim.boundsToGlobal = function(obj, rect, flip) {
		if (!zim.boundsCheck) {z_d("40"); zim.boundsCheck = true;}
		if (zot(obj) || !obj.getBounds) return;
		if (zot(flip)) flip = false;
		var oB = obj.getBounds();
		if (!oB && zot(rect)) {
			zog("zim methods - boundsToGlobal():\n please setBounds() on object (or a rectangle)");
			return;
		}
		if (rect) oB = rect;

		if (flip) {
			var pTL = obj.globalToLocal(oB.x, oB.y);
			var pTR = obj.globalToLocal(oB.x+oB.width, oB.y);
			var pBR = obj.globalToLocal(oB.x+oB.width, oB.y+oB.height);
			var pBL = obj.globalToLocal(oB.x, oB.y+oB.height);
		} else {
			var pTL = obj.localToGlobal(oB.x, oB.y);
			var pTR = obj.localToGlobal(oB.x+oB.width, oB.y);
			var pBR = obj.localToGlobal(oB.x+oB.width, oB.y+oB.height);
			var pBL = obj.localToGlobal(oB.x, oB.y+oB.height);
		}

		// handle rotation
		var newTLX = Math.min(pTL.x,pTR.x,pBR.x,pBL.x);
		var newTLY = Math.min(pTL.y,pTR.y,pBR.y,pBL.y);
		var newBRX = Math.max(pTL.x,pTR.x,pBR.x,pBL.x);
		var newBRY = Math.max(pTL.y,pTR.y,pBR.y,pBL.y);

		return new createjs.Rectangle(
			newTLX,
			newTLY,
			newBRX-newTLX,
			newBRY-newTLY
		);
	}//-40

/*--
obj.resetBounds = function(width||boundsX, height||boundsY, null||width, null||height)

resetBounds
zim DisplayObject method

DESCRIPTION
Pass in no parameters to reset the bounds to calculated bounds
Pass in width and height to set bounds to (0,0,width,height)
Pass in x, y, width and height to set bounds to (0,0,width,height)

NOTE: a container made with new Container() has auto calculated bounds
so the bounds (and width and height) adapt to surround any new children's bounds
A container set with bounds to start: new Container(100, 200) has static bounds
so the bounds (and width and height) will not change as new children are added
Calling container.resetBounds() with no parameter sets dynamic calculated bounds
Or you can reset the static bounds with parameters.

EXAMPLE
var container = new Container();
var rect = new Rectangle(500,300).addTo(container);
zog(container.width, container.height); // 500, 300 dynamic bounds

var container = new Container(100, 100);
var rect = new Rectangle(500,300).addTo(container);
zog(container.width, container.height); // 100, 100 static bounds

container.resetBounds();
zog(container.width, container.height); // 500, 300 dynamic bounds

container.resetBounds(200, 200);
rect.removeFrom();
zog(container.width, container.height); // 200, 200 static bounds
END EXAMPLE

PARAMETERS
width - (default null) null will set dynamic bounds and the object will take on bounds of children if a Container
	setting a number for the width will set static bounds that do not change as objects are added
height - (default width) the height of the bounds
   if there is a width supplied but no height then the height is set to the width
   setting these run obj.setBounds(boundsX,boundsY,width,height);

OR if four parameters are set:
boundsX - (default 0) the x of the bounds
boundsY - (default 0) the y of the bounds
width - (default null) the width of the bounds
height - (default width) the height of the bounds
   if there is a width supplied but no height then the height is set to the width
   setting these run obj.setBounds(boundsX,boundsY,width,height);

RETURNS object for chaining
--*///+40.5
	zim.resetBounds = function(obj, a, b, c, d) {
		z_d("40.5");
		if (!zot(c)) {
			var boundsX = a;
			var width = c;
			var boundsY = b;
			var height = d;
		} else {
			var boundsX = 0;
			var width = a;
			var boundsY = 0;
			var height = b;
		}
		if (zot(height)) height = width;
		if (!zot(a)) obj.setBounds(boundsX,boundsY,width,height);
		else obj.setBounds(null);
	return obj;
}//-40.5

/*--
obj.copyMatrix = function(source)

copyMatrix
zim DisplayObject method

DESCRIPTION
Copies the transformation properties from the source to the obj
(x, y, rotation, reg, scale and skew)

NOTE: used internally by animate() and setMask() for copying transform of shapes to mask
also used in addDisplayMembers for clone() method

EXAMPLE
circle.copyMatrix(circle2);
// circle will now match circle2 in x, y, rotation, scale and skew properties
END EXAMPLE

PARAMETERS
source - object from which the transform properties are being copied

RETURNS obj for chaining
--*///+45.5
	zim.copyMatrix = function(obj, source) {
		z_d("45.5");
		obj.x = source.x;
		obj.y = source.y;
		obj.scaleX = source.scaleX;
		obj.scaleY = source.scaleY;
		obj.regX = source.regX;
		obj.regY = source.regY;
		obj.rotation = source.rotation;
		obj.skewX = source.skewX;
		obj.skewY = source.skewY;
		return obj;
	}//-45.5

/*--
obj.duplicate = function()

duplicate
zim DisplayObject method

DESCRIPTION
clones the object but also any custom properties

EXAMPLE
var circle = new Circle();
circle.custom = 345;
var circle2 = circle.duplicate();
zog(circle2.custom); // 345
END EXAMPLE

RETURNS cloned object with cloned custom properties
--*///+45.6
	zim.duplicate = function(obj) {
		z_d("45.6");
		var d = obj.clone();
		for (var prop in obj) {
			if (obj.hasOwnProperty(prop) && d["prop"]==null) {
				d[prop] = obj[prop];
			}
		}
		return d;
	}//-45.6

/*--
obj.expand = function(padding, paddingVertical)

expand
zim DisplayObject method

DESCRIPTION
Adds a createjs hitArea to an object with an extra padding of padding.
Good for making mobile interaction better on labels, buttons, etc.

EXAMPLE
var circle = new Circle(10, "red");
circle.center(stage);
circle.expand(); // makes hit area bigger
circle.on("click", function(){zog("yes");});
END EXAMPLE

PARAMETERS
padding - (default 20) how many pixels to expand bounds
paddingVertical - (default null) the vertical padding (making padding for horizontal)

RETURNS obj for chaining
--*///+50
	zim.expand = function(obj, padding, paddingVertical) {
		z_d("50");
		if (zot(obj) || !obj.getBounds || !obj.getBounds()) {zog("zim methods - expand(): please provide object with bounds set"); return obj;}
		if (zot(padding)) padding = 20;
		if (zot(paddingVertical)) paddingVertical = padding;
		var oB = obj.getBounds();
		var rect = new createjs.Shape();
		rect.graphics.f("0").r(oB.x-padding,oB.y-paddingVertical,oB.width+2*padding,oB.height+2*paddingVertical);
		obj.hitArea = rect;
		return obj;
	}//-50

/*--
obj.setSwipe = function(swipe)

setSwipe
zim DisplayObject method

DESCRIPTION
Sets whether we want to swipe an object or not using ZIM Swipe.
Recursively sets children to same setting.

EXAMPLE
circle.swipe();
END EXAMPLE

PARAMETERS
swipe - (default true) set to false to not swipe object

RETURNS obj for chaining
--*///+34
	zim.setSwipe = function(obj, swipe) {
		z_d("34");
		if (zot(obj) || !obj.on) return;
		obj.zimNoSwipe = (swipe) ? null : true;
		if (obj instanceof createjs.Container) dig(obj);
		function dig(container) {
			var num = container.numChildren;
			var temp;
			for (var i=0; i<num; i++) {
				temp = container.getChildAt(i);
				temp.zimNoSwipe = obj.zimNoSwipe;
				if (temp instanceof createjs.Container) {
					dig(temp);
				}
			}
		}
		return obj;
	}//-34

/*--
obj.setMask = function(mask, dynamic)

setMask
zim DisplayObject method

DESCRIPTION
Specifies a mask for an object - the object can be any display object.
The mask can be a ZIM (or CreateJS) Shape or a ZIM Rectangle, Circle, Triangle or Blob.
Masking must be done with a Shape and the ZIM shapes are actually containers with Shape objects in them.
So setMask() takes care of all the arrangements and updates the mask when using the following ZIM features:
drag(), animate(), gesture(), transform() and using the Bezier curves, etc. with Blob.

NOTE: a Bitmap can be used for masking using the CreateJS AlphaMaskFilter
See https://zimjs.com/explore/alphaMask.html and https://zimjs.com/explore/mask.html

NOTE: the mask you pass in can still be seen but you can set its alpha to 0
just watch, if you want to interact with the mask it cannot have 0 alpha
unless you provide a hit area with expand() for instance (use 0 for padding)
You can also set the alpha to .01

NOTE: before ZIM 6.7.1, setMask could not be chained - but now it can

EXAMPLE
var label = new Label("BIG", 200, null, "white");
label.center(stage);
var rect = new Rectangle(200,100,"black");
rect.center(stage).alpha = 0;
var label = new Label("BIG", 200, null, "white")
	.center(stage)
	.drag()
	.setMask(rect);
END EXAMPLE

NOTE: to drag something, the alpha cannot be 0
so we can use expand(rect, 0) to assign a hit area
then we can drag even if the alpha is 0 (or set the alpha to .01)

EXAMPLE
var label = new Label("BIG", 200, null, "white").center(stage);
var rect = new Rectangle(200,100,"black")
	.expand(0)
	.center(stage)
	.alp(0)
	.drag();
label.setMask(rect);
END EXAMPLE

NOTE: animate(), drag(), gesture() and transform() work specially with zim shapes to make this work
otherwise, if you want to reposition your mask then set the dynamic parameter to true
or use a zim.Shape() or createjs.Shape directly to avoid this issue

EXAMPLE
// masking with a Blob
var image = frame.asset("pic.jpg").centerReg(stage);
var blob = new Blob({color:faint}).center(stage); // this is draggable by default
image.setMask(blob);
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
mask - the object whose shape will be the mask
dynamic - (default false) set to true if mask is being moved
	Blobs and shapes with drag(), transform(), gesture() and animate() will auto set dynamic to true if dynamic parameter is left empty
	Setting dynamic to false for these will remain with a setting of dynamic false and the mask will not move once set

NOTE: use obj.setMask(null) to clear the mask

RETURNS the object for chaining
Older versions returned the mask shape - the mask shape can now be accessed by obj.zimMask or mask.zimMask
--*///+50.1
	zim.setMask = function(obj, mask, dynamic) {
		z_d("50.1");
		if (zot(obj)) {zog("zim methods - setMask(): please provide obj");}
		if (zot(mask)) {setMaskNull(); return obj;}
		var stage = obj.stage||(zimDefaultFrame?zimDefaultFrame.stage:null);
		function setMaskNull() {
			if (obj.zimMask && obj.zimMask.parent) obj.zimMask.parent.removeChild(obj.zimMask);
			obj.zimMask = null;
			if (obj.zimMaskTicker) zim.Ticker.remove(obj.zimMaskTicker);
			obj.mask = null;
		}
		if (obj.zimMaskOriginal && obj.zimMaskOriginal != mask) setMaskNull();
		if (zot(dynamic)) {
			// blob, drag, transform, gesture, animate
			dynamic = (mask.type=="Blob" || !zot(mask.zimDown) || !zot(mask.transformControls) || !zot(mask.zimTouch) || !zot(mask.zimTweens));
			mask.zimMaskDynamic = dynamic;
		}
		obj.zimMaskOriginal = mask;
		var m;
		function apply() {
			if (dynamic && (!stage || !mask.stage)) return obj;
			m = null;
			obj.zimMask = mask.zimMask = m = mask.shape.clone();
			zim.copyMatrix(m, mask);
			mask.addChildAt(m,0);
			m.alpha = 0;

			if (!obj.parent) {obj.mask = m; return;}
			var point = mask.localToLocal(mask.regX,mask.regY,obj.parent);
			var mm = mask.getConcatenatedMatrix().decompose();
			var om = obj.parent.getConcatenatedMatrix().decompose();
			m.scaleX = mm.scaleX/om.scaleX;
			m.scaleY = mm.scaleY/om.scaleY;
			m.rotation = (mm.rotation?mm.rotation:0)-(om.rotation?om.rotation:0);

			m.x = point.x;
			m.y = point.y;
			if (obj.parent == mask.parent) {
				m.x = mask.x+mask.shape.x;
				m.y = mask.y+mask.shape.y;
			}
			obj.mask = m;
		}
		// called from Blob and zim functions that move zim shapes
		mask.zimMaskApply = function() {
			if (obj.zimMaskTicker) zim.Ticker.remove(obj.zimMaskTicker);
			obj.zimMaskTicker = zim.Ticker.add(apply);
		}
		if (mask && mask.shape) { // zim.Rectangle, Circle or Triangle, blob
			apply();
			if (dynamic) mask.zimMaskApply();
		} else {
			m = mask;
			obj.mask = m; // set the createjs mask
		}
		return obj;
	}//-50.1

/*--
obj.outline = function(color, size)

outline
zim DisplayObject method

DESCRIPTION
For testing purposes.
Draws a rectangle around the bounds of obj (adds rectangle to the objects parent).
Draws a cross at the origin of the object (0,0) where content will be placed.
Draws a circle at the registration point of the object (where it will be placed in its container).
These three things could be in completely different places ;-)

NOTE: warning - the outline is added to the object's container so this can affect the container's numChildren, etc.

NOTE: will not subsequently be resized unless called again in Ticker, pressmove event, etc.
ZIM could update this outline automatically in drag and animate
but since the outline() is used for testing, it is not a good idea to waste processing

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage);
// show registration and origin at center and bounding box around outside
circle.outline();
END EXAMPLE

EXAMPLE
Dynamic Examples
var ball = new Circle().center().drag({removeTweens:false});
ball.on("mousedown", function(){
	ball.outline();
});
ball.on("pressmove", function(){
	ball.outline();
});
ball.animate({rotation:360}, 4000);
Ticker.add(function() {ball.outline();});
END EXAMPLE

EXAMPLE
// When applying outline() in a loop - loop in reverse
// as outlines are added to the object's container
var tile = new Tile(new Rectangle(), 5, 4, 10, 10).center();
tile.loop(function (item) {
	item.outline();
}, true); // reverse loop
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
color - (default brown) the color of the outline
size - (default 2) the stroke size of the outline

PROPERTIES
adds a ZIMoutlineShape and ZIMoutlineShapeC to object
adds an outlineToggled read only property that is true if showing and false if not showing outline

METHODS
adds an outlineToggle(state) method to the object that toggles the outline when called
	or can pass in true to show outline or false to not show outline
	note - method does not update the stage
	returns object for chaining

RETURNS the obj for chaining;
--*///+47
	zim.outline = function(obj, color, size) {
		var sig = "obj, color, size";
		var duo; if (duo = zob(zim.outline, arguments, sig)) return duo;
		z_d("47");
		if (obj.type && obj.type == "zimOultineShape") {
			if (zon) zog ("zim.outline() - warning, you are trying to outline an outline - do not outline in a loop");
			return obj;
		}
		if (zot(obj) || !obj.getBounds) {zog("zim methods - outline(): please provide object with bounds set"); return obj;}
		if (!obj.getBounds()) {zog("zim methods - outline(): please setBounds() on object");	return obj;}
		if (!obj.parent) {zog("zim methods - outline(): object should be on stage first"); return obj;}
		if (zot(color)) color = "brown";
		if (zot(size)) size = 2;
		if (size <= 0) {
			if (obj.ZIMoutlineShape) obj.ZIMoutlineShape.parent.removeChild(obj.ZIMoutlineShape);
			if (obj.ZIMoutlineShapeC) obj.ZIMoutlineShapeC.parent.removeChild(obj.ZIMoutlineShapeC);
			obj.ZIMoutlineShape = obj.ZIMoutlineShapeC = null;
			return;
		}
		var oB = obj.getBounds();
		var shape = obj.ZIMoutlineShape = (obj.ZIMoutlineShape)?(obj.ZIMoutlineShape):new zim.Shape();
		shape.type = "zimOutlineShape";
		var shapeC = obj.ZIMoutlineShapeC = (obj.ZIMoutlineShapeC)?(obj.ZIMoutlineShapeC):new zim.Shape();
		shapeC.type = "zimOutlineShape";
		var p = obj.parent;
		obj.outlineToggled = true;
		obj.outlineToggle = function(type) {
			if (type === true) {
				obj.ZIMoutlineShape.visible = true;
				obj.ZIMoutlineShapeC.visible = true;
			} else if (type === false) {
				obj.ZIMoutlineShape.visible = false;
				obj.ZIMoutlineShapeC.visible = false;
			} else {
				obj.ZIMoutlineShape.visible = !obj.ZIMoutlineShape.visible;
				obj.ZIMoutlineShapeC.visible = !obj.ZIMoutlineShapeC.visible;
			}
			obj.outlineToggled = obj.ZIMoutlineShape.visible;
			return obj;
		}

		var pTL = obj.localToLocal(oB.x, oB.y, p);
		var pTR = obj.localToLocal(oB.x+oB.width, oB.y, p);
		var pBR = obj.localToLocal(oB.x+oB.width, oB.y+oB.height, p);
		var pBL = obj.localToLocal(oB.x, oB.y+oB.height, p);
		var pC = obj.localToLocal(0, 0, p);

		var g = shape.graphics.c();
		var gC = shapeC.graphics.c();
		g.s(color).ss(size)
			.mt(pTL.x, pTL.y)
			.lt(pTR.x, pTR.y)
			.lt(pBR.x, pBR.y)
			.lt(pBL.x, pBL.y)
			.lt(pTL.x, pTL.y)
			.cp();

		// subtract a scaled top left bounds from the top left point
		// zero = {x:pTL.x-oB.x*obj.scaleX, y:pTL.y-oB.y*obj.scaleY};

		// cross at 0 0
		var s = 10;
		var ss = s+1;
		gC.s("white").ss(size+2);
		gC.mt(-ss, 0).lt(ss, 0);
		gC.mt(0, -ss).lt(0, ss);
		gC.s(color).ss(size);
		gC.mt(-s, 0).lt(s, 0);
		gC.mt(0, -s).lt(0, s);
		shapeC.x = pC.x;
		shapeC.y = pC.y;
		shapeC.rotation = obj.rotation;

		// circle at registration point
		g.s("white").ss(size+2).dc(obj.x,obj.y,s+6);
		g.s(color).ss(size).dc(obj.x,obj.y,s+6);

		obj.parent.removeChild(shape);
		obj.parent.removeChild(shapeC);
		var index = obj.parent.getChildIndex(obj);
		obj.parent.addChildAt(shapeC, index+1);
		obj.parent.addChildAt(shape, index+1);


		shape.mouseEnabled = false;
		shapeC.mouseEnabled = false;
		// if (obj.stage) obj.stage.update();
		return obj;
	}//-47


////////////////  ZIM CONTROLS  //////////////

// Zim Controls (formerly Zim Pages) helps you layout and control flexive pages, click and swipe between pages and more
// classes in this module require createjs namespace to exist and in particular easel.js
// available at http://createjs.com

/*--
zim.STYLE

STYLE
zim constant

DESCRIPTION
STYLE can be used to set any parameter on a DisplayObject and many of the Controls.
For instance: Circle, Blob, Button, Pane, Bitmap, Sprite, Tile, Pen, Emitter, Scroller, etc.
These are applied at the time the objects are made.
They are cascading with each level overriding the previous level:

1. GENERAL: any style can be specified in general
	corner:30 will make all corners default to 30
2. TYPE: styles for object type can be set to override the general styles
	Button:{corner:0} will make all button corners default to 0
3. GROUP: styles for a group can be set to override the type styles
	homePage:{corner:20} will make all objects of that group default to 20
4. OBJECT: styles applied as parameters to the object override all other styles
	new Button({corner:40}) will make this button have a corner of 40

See: https://zimjs.com/style.html for an example
And: https://zimjs.com/test/styles.html for Control Styles

EXAMPLE
STYLE = {
	corner:20,
	backgroundColor:pink,
	type:{
		Button:{width:{min:100, max:500}, corner:0, centerReg:true, move:{y:series([-150, -50, 50, 150])}},
		Dial:{add:true, x:800, y:600, backgroundColor:red, scale:2, outline:true},
		Pane:{corner:ignore, color:white, draggable:true, width:300, label:"HI!"},
		ProgressBar:{add:true, x:200, y:600, barType:"rectangle", transform:true},
		Tabs:{add:true, x:100, y:100}
	},
	group:{
		homePage:{corner:30}
	}
}
new Button(); // will have a corner of 0 and be pink
new Button({group:"homePage"}); // will have a corner of 30 and be pink
new Button({corner:10, group:"homePage"}); // will have a corner of 10 and be pink
new Button({corner:"ignore"}) // will have a corner of its default 20 and be pink
new Button({style:false}).pos(700,100); // will have original default styles
new Dial(); // will be red and scaled twice as big and have an outline
new Tabs(); // will have a corner of 20 and selection will be pink
var p = new ProgressBar({corner:15}); // will be a bar with transform tools, corner 15
p.percent = 25;
new Pane().show(); // will ignore corner 30 and use its original 20 - it will say HI! in white and be draggable
END EXAMPLE

EXAMPLE
STYLE = {
	borderColor:dark,
	borderWidth:5,
	type:{
		Rectangle:{
			// color:red,
			color:[red, pink, purple], // pick a random color from here
			centerReg:true,
			animate:{props:{rotation:360}, time:500, ease:"linear", loop:true},
			move:{x:series([-200, 0, 200])}
		}
	}
}
// make three spinning rectangles
loop(3, function(){new Rectangle();});
END EXAMPLE

TRANSFORM STYLES
Transformations can also be applied (all are numbers - visible is a Boolean):
x, y, rotation, alpha, scale, scaleX, scaleY, regX, regY, skewX, skewY, visible
a bounds style has a value of {x:Number, y:Number, width:Number, height:Number}
where x and y are optional

RANDOM, RANGES, AND SERIES STYLES
Any property value can be a ZIM VEE value to make use of ZIM Pick see https://zimjs.com/docs.html?item=Pick
color:[red, green, blue] will pick a random color for each object for which the style is applied
x:series([100,200,300]) will place subsequent objects at these locations
width:{min:100, max:500} will make objects with a width within this range
corner:{noPick:[0,40,0,40]}; will set corners - note we have to noPick this otherwise it would pick a number from the array for the corner value
See Docs on ZIM Pick() and ZIM series() for more information
Use the delayPick:true style to pass a PICK object to a control
See: https://zimjs.com/explore/circleArcs.html

FUNCTION STYLES
The following functions have been added:
addTo, loc, pos, center, centerReg, transform, drag, gesture, outline, mov, animate, wiggle and expand
Values of true will give default functionality for all but mov, animate and wiggle
ZIM DUO configuration objects can be set as a value for any of these
example: drag:true;  or  drag:{onTop:false}
For animate and wiggle, [] can be put around multiple configuration objects
to wiggle in the x and y for instance or run multiple animate calls on the object

CONVENIENCE STYLES
add:true - has been provided to add to the stage (use addTo for other containers)
move:{x:value, y:value} or move:x - mirrors the mov Function style (just adding the e)
pos: has corner convenience values: "left", "right", "top", "bottom", "rightbottom" or "bottomright", "center" and "centerReg"
style:false - will turn off all styles for the selector

EXCLUSION
A value of ignore can be used to exclude any earlier styles and return to the original default.
ignore is a ZIM global variable - if zns is true then use zim.ignore or just "ignore".
Setting style:false will exclude the object from all styles
All DisplayObjects have a style parameter (default true). Set to false to ignore styles.

GROUPS
All DisplayObjects have a group parameter.
This parameter accepts a string or a comma delimited string of multiple groups.
The group of components can then be targeted in the Group section of STYLE.
A group can contain just one component and act like an ID in CSS.

INHERIT
Objects can inherit styles from their parent
Eg. setting a color font property on List will pass that to Tabs which pass it to Button which passes it to Label
This was initially set up manually but a new inherit parameter was created for a better system
If any styles are not being applied as expected or as desired, let us know at https://zimjs.com/slack and we can adjust!
The inherit property on the Components can be used to pass an object literal of styles to the component directly as well

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

--*///+50.34
zim.STYLE = null;
zim.STYLECHECK = false;
zim.ignore = "ignore";
zim.getStyle = function(type, group, inherit) {
	if (!zim.STYLECHECK) {z_d("50.34"); zim.STYLECHECK = true;}

	var functionList = ["pos","addTo","center","centerReg","mov","drag","transform","gesture","outline","bounds","animate","wiggle"];

	// called by DisplayObjects
	var DS = zim.STYLE;
	if (typeof STYLE !== typeof undefined) DS = STYLE;

	if (!zot(DS)) DS = zim.copy(DS); // can't copy with clone due to recursion
	else DS = {};

	// an object of styles from creator class that have already been ziked
	// for instance, List calls Window and Tabs and these can inherit styles applied to the List
	// Tabs call Buttons and Labels and these can inherit styles from the List and Tabs
	// List will overwrite and Button styles - so originator overwrites subsequent styles

	if (zot(inherit)) {
		inherit = {};
	} else {
		// remove any functions from inheritance
		for (var i=0; i<functionList.length; i++) {
			delete inherit[functionList[i]];
		}
	}
	var IS = inherit;

	var TS = DS.type;
	if (TS) delete DS.type;
	if (!zot(group) && !zot(DS.group)) {
		var groups = group.split(","); // support comma delimited multiple groups
		var GS = {};
		for (var i=0; i<groups.length; i++) {
			var g = groups[i].trim();
			if (!zot(DS.group[g])) GS = zim.merge(GS, DS.group[g]);
		}
		delete DS.group;
	}

	if (!zot(type) && !zot(TS) && !zot(TS[type])) DS = zim.merge(DS, TS[type]);
	if (!zot(group) && !zot(GS)) DS = zim.merge(DS, GS);
	var val;

	for (var s in DS) {
		if (IS[s]) { // inherited value overrides
			delete DS[s];
			continue;
		}
		val = functionList.indexOf(s)==-1&&!DS.delayPick?zim.Pick.choose(DS[s]):DS[s]; // do not zik configuration objects of methods
		if (val=="ignore") {delete DS[s]; continue;}
		// not sure what the line below was for - maybe auto cloning a bitmap? but had to remove it ZIM 9.5.0 when debugging addTo STYLE issues
	 	// else if (val && val.clone && val.type != type && val.type != "Stage" && val.type != "StageGL") val = val.clone();
		// copy config objects with clone except for type container - do not center or centerReg on shape
		else if (val && val.constructor === {}.constructor) val = zim.copy(val, true, false);
		DS[s] = val;
	}
	DS = zim.merge(DS, IS);

	if (DS.style===false) return {};

	return DS;
}

// sets x, y, alpha, rotation, scale, scaleX, scaleY, regX, regY, skewX, skewY, bounds, visible
// also sets addTo, center, centerReg, transform, drag, gesture, animate, wiggle, and outline (used internally)
var zimTransformList = ["visible","x","y","scale","scaleX","scaleY","rotation","alpha","skewX","skewY","regX","regY"];
var zimStyleTransforms = function(obj, styles) {
	if (styles) {
		if (styles.add) {
			obj.addTo();
		}
		if (styles.addTo) {
			obj.addTo(styles.addTo===true?null:styles.addTo);
			// not sure what these were doing below but replaced them with above... ZIM 9.5.0
			//obj.addTo(styles.addTo.constructor==={}.constructor?styles.addTo:null);
		}
		if (styles.center) {
			obj.center(styles.center===true?null:styles.center);
			// obj.center(styles.center.constructor==={}.constructor?styles.center:null);
		}
		if (styles.centerReg) {
			obj.centerReg(styles.centerReg===true?null:styles.centerReg);
			//obj.centerReg(styles.centerReg.constructor==={}.constructor?styles.centerReg:null);
		}
		for (var i=0; i<zimTransformList.length; i++) {
			if (styles[zimTransformList[i]]!=null) obj[zimTransformList[i]] = zim.Pick.choose(styles[zimTransformList[i]]);
		}
		if (styles.bounds) {
			if (styles.bounds.constructor==={}.constructor) {
				obj.setBounds(styles.bounds.x?styles.bounds.x:0, styles.bounds.y?styles.bounds.y:0, styles.bounds.width?styles.bounds.width:100, styles.bounds.height?styles.bounds.height:100);
			}
		}
		if (styles.mov || styles.move) {
			if (styles.move) styles.mov = styles.move;
			if (styles.mov.constructor==={}.constructor) {
				obj.mov(zim.Pick.choose(styles.mov.x), zim.Pick.choose(styles.mov.y));
			} else {
				obj.mov(zim.Pick.choose(styles.mov));
			}
		}
		if (styles.loc) {
			if (styles.loc.constructor==={}.constructor) {
				obj.loc(zim.Pick.choose(styles.loc.x), zim.Pick.choose(styles.loc.y));
			} else {
				obj.loc(zim.Pick.choose(styles.loc));
			}
		}
		if (styles.pos) {
			if (styles.pos.constructor==={}.constructor) {
				obj.pos(zim.Pick.choose(styles.pos.x), zim.Pick.choose(styles.pos.y), zim.Pick.choose(styles.pos.right), zim.Pick.choose(styles.pos.bottom), zim.Pick.choose(styles.pos.index), styles.pos.add, zim.Pick.choose(styles.pos.reg), zim.Pick.choose(styles.pos.regX), zim.Pick.choose(styles.pos.regY));
			} else {
				var sp = zim.Pick.choose(styles.pos);
				if (sp == "left" || sp == "top") obj.pos();
				if (sp == "right") obj.pos({right:true});
				if (sp == "bottom") obj.pos({bottom:true});
				if (sp == "rightbottom" || sp == "bottomright") obj.pos({right:true, bottom:true});
				if (sp == "center") obj.center();

				// if (styles.pos == "left" || styles.pos == "top") obj.pos();
				// if (styles.pos == "right") obj.pos({right:true});
				// if (styles.pos == "bottom") obj.pos({bottom:true});
				// if (styles.pos == "rightbottom" || styles.pos == "bottomright" || styles.pos == "rightBottom" || styles.pos == "bottomRight") obj.pos({right:true, bottom:true});
				// if (styles.pos == "center") obj.center();
				// if (styles.pos == "centerReg" || styles.pos == "centerreg") obj.centerReg();
			}
		}
		if (styles.outline) {
			setTimeout(function(){
				obj.outline(styles.outline.constructor==={}.constructor?styles.outline:null);
				if (obj.stage) obj.stage.update();
			},20);
		}
		if (styles.drag) {
			obj.drag(styles.drag.constructor==={}.constructor?styles.drag:{currentTarget:true});
		}
		if (styles.gesture) {
			obj.gesture(styles.gesture.constructor==={}.constructor?styles.gesture:null);
		}
		if (styles.transform) {
			setTimeout(function(){obj.transform(styles.transform.constructor==={}.constructor?styles.transform:null);},20);
		}
		if (styles.animate) {
			if (!Array.isArray(styles.animate)) styles.animate = [styles.animate];
			for (var i=0; i<styles.animate.length; i++) {
				-function(){
					var sty = styles.animate[i];
					if (sty.constructor==={}.constructor) setTimeout(function(){obj.animate(sty);},20);
				}();
			}
		}
		if (styles.wiggle) {
			if (!Array.isArray(styles.wiggle)) styles.wiggle = [styles.wiggle];
			for (var i=0; i<styles.wiggle.length; i++) {
				-function(){
					var sty = styles.wiggle[i];
					if (sty.constructor==={}.constructor) setTimeout(function(){obj.wiggle(sty);},20);
				}();
			}
		}
		if (styles.expand) {
			obj.expand(styles.expand===true?null:styles.expand);
		}
	}
}
//-50.34

/*--
zim.ANIMATE

ANIMATE
zim constant

DESCRIPTION
Set to false to stop animate() calls from working.
Handy for testing your app so you do not have to wait for animations every time!
To animate things in you can place everything in their final positions
and then set the "from" parameter to true to animate from starting positions
like x or y offstage, scale or alpha of 0, etc.
Then to avoid waiting for animations to complete, you can just set ANIMATE = false
and all your objects will be in their final locations and you don't wait for animations
When you are ready to run your animations for a final version, etc. just delete the line
or set ANIMATE to true.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
ANIMATE = false;
// without the line above, the circles will animate in
// we would have to wait for them every time we load the app
// sometimes animations are even longer and this can waste development time
// when we add the line above, the circles are on stage right away
// this is easier and safer than commenting out all your animations

var circle1 = new Circle(200, green);
circle1.center(stage);
circle1.x -= 110;
circle1.animate({props:{alpha:0, scale:0}, time:700, from:true});

var circle2 = new Circle(200, pink);
circle2.center(stage);
circle2.x += 110;
circle2.animate({props:{alpha:0, scale:0}, time:700, wait:700, from:true});
END EXAMPLE
--*///+29.5
zim.ANIMATE = true;
//-29.5

/*--
zim.OPTIMIZE

OPTIMIZE
zim constant

DESCRIPTION
A setting that relates to how stage.update() is used by the components.
Default is false which means some components will update the stage automatically:
	the Slider will update the stage so that you can see the knob slide;
	the CheckBox and RadioButtons when checked will update the stage;
	the Tabs change button colors and then update the stage;
	closing of a Pane will update the stage
	the Stepper also updates as does changing color of a button, label, etc.
However, concurrent stage.update() calls can slow down mobile performance.
So if you are making content for mobile you should set OPTIMIZE = true;
Then you will have to stage.update() in the change event handlers
but you were probably doing things in these events and updating anyway!
Just be careful - you might be testing a checkbox and it won't check...
So it takes some getting used to running in optimized mode.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// add this to the top of your script
OPTIMIZE = true;
var slider = new Slider();
slider.center(stage);
// will not see the slider operate (aside from rolling over button)
// unless you call stage.update() in the change event
slider.on("change", function() {
	// do your code
	stage.update(); // now will see the slider operate
});
END EXAMPLE

components affected by OPTIMIZE:
Label, Button, Checkbox, RadioButton, Pane, Stepper, Slider, Tabs

OPTIMIZE set to true also affects the ZIM Ticker
for functions like animate, drag, Scroller, Parallax
See Ticker as you may have to set Ticker.update = true;
--*///+50.2
zim.OPTIMIZE = false;
//-50.2

/*--
zim.ACTIONEVENT

ACTIONEVENT
zim constant

DESCRIPTION
a setting that specifies the event type to trigger many of the components
default is "mousedown" which is more responsive on mobile
setting the constant to anything else, will cause the components to use "click"

for instance, with the default settings, the following components will act on mousedown
CheckBox, RadioButtons, Pane, Stepper and Tabs

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// put this at the top of your code
ACTIONEVENT = "click";
var checkBox = new CheckBox();
checkBox.center(stage);
// note it now operates on mouseup (click)
// the default ACTIONEVENT is mousedown
END EXAMPLE
--*///+50.3
zim.ACTIONEVENT = "mousedown";
//-50.3

/*--
zim.KEYFOCUS

KEYFOCUS
zim global variable (in guise of a constant)

DESCRIPTION
A global variable that stores the DisplayObjects that will receive keyboard focus.
This is a system for all DisplayObjects with keyboard controls (set to active).
Eligible DisplayObjects are:
 	Squiggle, Blob, Stepper, Slider, Dial, Tabs, ColorPicker, TextArea
The first eligible DisplayObject made will set KEYFOCUS to itself.
Anytime an eligible DisplayObject is used it sets KEYFOCUS to itself.
Eligable DisplayObjects have a keyFocus property that can be manually set.
There is only one DisplayObject with key focus
so setting removes key focus from the last key focused component.

NOTE: if tabbing from one component to the next is needed, consider using ZIM Accessibility()

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var slider = new Slider().center().mov(0, -100);
var stepper = new Stepper().center().mov(0, 100);
// arrows will control the slider as it is the first component made
// if the stepper is used then arrows will control the stepper
// if stepper.keyFocus = true is set to start then the stepper would have key control
// alternatively, KEYFOCUS = stepper; would give the stepper key control
// setting KEYFOCUS = null; would make no component have key control (until the component is pressed)
END EXAMPLE
--*///+50.33
zim.KEYFOCUS = null;
//-50.33

/*--
zim.POSREG

POSREG
zim global variable (in guise of a constant)

DESCRIPTION
A global variable that stores the desired setting for the reg parameter of pos()
pos() had traditionally positioned based on registration point
Now it defaults to position to the sides or the top and bottom
Setting POSREG = true; will make pos() default to positioning at the registration

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
POSREG = false; // default
new Rectangle().pos({x:10, right:true}); // will position right side 10 pixels from right

POSREG = true;
new Rectangle().pos({x:10, right:true}); // will position registration point 10 pixels from right
END EXAMPLE
--*///+50.335
zim.POSREG = false;
//-50.335

/*--
zim.DRAGALL

DRAGALL
zim global variable (in guise of a constant)

DESCRIPTION
A global variable that stores the desired setting for the all parameter of drag()
By default, drag() will drag individual items in a container (currentTarget = false)
This is handy to quickly drag a bunch of things.
But some developers are used to dragging the whole container by default.
Setting DRAGALL=true will change drag calls to default to the whole container (currentTarget = true)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
DRAGALL = false; // default
containerOfMonsters.drag(); // will drag individual monsters

DRAGALL = true;
containerOfMonsters.drag(); // will drag all the monsters at once
END EXAMPLE
--*///+50.337
zim.DRAGALL = false;
//-50.337

/*--
zim.Ticker = {}

Ticker
zim static class

DESCRIPTION
A static class to let ZIM use a requestAnimationFrame with a single stage update.
Used by ZIM animate, parallax, scrollers, etc. but any function can be added.
If a function has been added to the Ticker queue then it will run in the order added
along with a single stage update after all functions in queue have run.
There are settings that can adjust when the Ticker updates so see Usage notes below.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle(50, "red");
circle.center(stage);
Ticker.add(function(){
	circle.x++;
}, stage); // stage is optional - will be the first stage made if left out

// to be able to remove the function:
Ticker.add(tryMe, stage);
function tryMe() {circle.x++;}
Ticker.remove(tryMe);

// OR with function literal, use the return value
var tickerFunction = Ticker.add(function(){circle.x++;}, stage);
Ticker.remove(tickerFunction);

// Check to see if a function is in the Ticker for that stage:
zog(Ticker.has(tickerFunction)); // false at the moment until added again
END EXAMPLE

USAGE
if OPTIMIZE is true then the Ticker will not update the stage (it will still run functions)
however, OPTIMIZE can be overridden as follows (or with the always() method):

METHODS (static)
** As of ZIM 5.1.0, stage is optional and will default to the stage of first Frame object made
** WARNING - if you are in a second Frame you should pass stage as a paramter so it does not point to the first Frame's stage
Ticker.always(stage) - overrides OPTIMIZE and always runs an update for the stage even with no function in queue
Ticker.alwaysOff(stage) - stops an always Ticker for a stage
Ticker.add(function, stage) - adds the function to the Ticker queue for a given stage and returns the function that was added
	the function will receive a CreateJS tick object with delta, time, timeStamp, etc. properties
Ticker.remove(function) - removes the function from the Ticker queue
Ticker.removeAll([stage]) - removes all functions from the Ticker queue (optionally per stage)
Ticker.has(function, stage) - returns a Boolean true if function is currently added to the Ticker for the stage - or false if not currently added
Ticker.setFPS(30, 60) - (mobile, pc) default is set at natural requestAnimationFrame speed - this seems to be the smoothest
Ticker.setTimingMode(mode) - (default "raf") RAF uses RequestAnimationFrame without framerate synching - gets screen synch (smooth) and background throttling
	set to "synched" for framerate synching - but will add some variance between updates
	set to "timeout" for setTimeout synching to framerate - no screen synch or background throttling (if RAF is not supported falls back to this mode)
	see CreateJS docs: http://www.createjs.com/docs/tweenjs/classes/Ticker.html
Ticker.raw(function) - a stand-alone direct call to RequestAnimationFrame for maximum speed
	Example: https://zimjs.com/raw/
 	Does not use Dictionary lookup that the add() uses so provides ultimate speed for generative art, etc.
	Returns function as id so can use Ticker.removeRaw(id)
	function will receive a single parameter that is a DOMHighResTimeStamp
	raw() does not automatically update the stage so put a stage.update() in the function
	raw() is for when you want to run one function much like the draw() in Processing, the animate() renderer in ThreeJS, etc.
	add() is for when you want to run multiple functions with a single globally coordinated stage.update()
Ticker.removeRaw(id) - remove the raw function based on the return value of the var id = Ticker.raw(function)
Ticker.dispose([stage]) - removes all functions from the queue removes and removes the list (optionally per stage)

PROPERTIES (static)
Ticker.update = true - overrides OPTIMIZE and forces an update if a function is in the queue
Ticker.update = false - forces no update regardless of OPTIMIZE
Ticker.update = null (default) - only updates if there is a function in queue and OPTIMIZE is false
Ticker.list - a ZIM Dictionary holding arrays with the functions in the Ticker queue for each stage
Ticker.list.objects - the array of stages in the Ticker
Ticker.list.values - the array holding an array of functions for each stage in the Ticker
Ticker.framerate - read only - use setFPS() to set

the Ticker is used internally by zim functions like animate(), drag(), Scroller(), Parallax()
you are welcome to add functions - make sure to pass the stage in as a second parameter to the add() method

USAGE
1. if you have your own ticker going, just set OPTIMIZE = true and don't worry about a thing
2. if you do not have your own ticker going but still want OPTIMIZE true to avoid components updating automatically,
then set OPTIMIZE = true and set Ticker.update = true
this will run a single update only when needed in zim Ticker for any zim functions
3. if you want a ticker with a single update going all the time (with OPTIMIZE true or false) then
run Ticker.always(stage);
4. if for some reason (can't think of any) you want no ticker updates for zim but want component updates
then set OPTIMIZE = false and then set Ticker.update = false
5. if you want maximum speed use Ticker.raw(function) which flows directly through to a RequestAnimationFrame
--*///+30
	zim.Ticker = {
		stages:null,
		myUpdate: null,
		alwaysList:new zim.Dictionary(),
		list:new zim.Dictionary(),
		setFPS: function(m, d) {
			if (zot(m) && zot(d)) {
				m = 30; d = 60;
			} else if (zot(m)) {
				m = 30;
			} else if (zot(d)) {
				d = m;
			}
			zim.Ticker.framerate = createjs.Ticker.framerate = (zim.mobile()) ? m : d;
		},
		setTimingMode: function(mode) {
			createjs.Ticker.timingMode = createjs.Ticker.RAF;
			if (mode == "synched") createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED;
			if (mode == "timeout") createjs.Ticker.timingMode = createjs.Ticker.TIMEOUT;
		},
		add: function(f, s) {
			z_d("30");
			var t = zim.Ticker;
			if (t.has(f, s)) return f; // don't let the same function be added twice
			if (!t.framerate) t.setFPS();
			if (zot(s) || !s.update) s = zimDefaultFrame.stage;
			if (zot(f) || typeof f !== 'function') {zog("zim.Ticker.add() - only add functions"); return;}
			if (!t.ticker) t.ticker = createjs.Ticker.on("tick", t.call);
			if (t.list.at(s)) {t.list.at(s).push(f);} else {t.list.add(s, [f]);}
			return f;
		},
		rawID:{},
		raw: function(f) {
			z_d("30");
			var id = zim.makeID(7, "letters");
			-function raw(t) {f(t); zim.Ticker.rawID[id] = requestAnimationFrame(raw);}();
			return id;
		},
		removeRaw: function(id) {
			cancelAnimationFrame(zim.Ticker.rawID[id]);
			delete zim.Ticker.rawID[id];
		},
		call: function(e) {
			var t = zim.Ticker;
			var s, functions;
			for (var i=0; i<t.list.length; i++) {
				s = t.list.objects[i]; // stage
				functions = t.list.values[i]; // list of functions for the stage
				for (var j=0; j<functions.length; j++) {
					functions[j](e);
				}
				if (t.alwaysList.at(s)) {
					s.update();
				} else if (functions.length > 0) {
					if (zot(t.update) && (!zim.OPTIMIZE&&(zns||!OPTIMIZE))) {
						s.update();
					} else if (t.update) {
						s.update();
					}
				}
			}
			// may have no functions to run but always is turned on
			for (i=0; i<t.alwaysList.length; i++) {
				s = t.alwaysList.objects[i]; // stage
				if (t.list[s] == null) 	s.update(); // if functions then update is already handled
			}
		},
		always: function(s) {
			z_d("30");
			var t = zim.Ticker;
			if (!t.framerate) t.setFPS();
			if (zot(s) || !s.update) s = zimDefaultFrame.stage;
			t.alwaysList.add(s, true);
			if (!t.ticker) t.ticker = createjs.Ticker.on("tick", t.call);
		},
		alwaysOff: function(s) {
			var t = zim.Ticker;
			if (zot(s) || !s.update) s = zimDefaultFrame.stage;
			t.alwaysList.remove(s);
		},
		remove: function(f) {
			var t = zim.Ticker;
			if (zot(f) || typeof f !== 'function') {zog("zim.Ticker - only remove functions"); return;}
			var count = 0;
			var s;
			for (var i=0; i<t.list.length; i++) {
				s = t.list.objects[i]; // stage
				var index = t.list.values[i].indexOf(f);
				if (index > -1) {
					t.list.values[i].splice(index,1);
				}
				count+=t.list.values[i].length;
			}
			if (t.alwaysList.length > 0) return;
			if (count == 0) {createjs.Ticker.off("tick", t.ticker); t.ticker = null;}
		},
		removeAll: function(s) {
			var t = zim.Ticker;
			var count = 0;
			var st;
			for (var i=0; i<t.list.length; i++) {
				st = t.list.objects[i]; // stage
				if (zot(s) || s === st) {
					t.list.values[i] = [];
				}
				count+=t.list.values[i].length;
			}
			if (t.alwaysList.length > 0) return;
			if (count == 0) {createjs.Ticker.off("tick", t.ticker); t.ticker = null;}
		},
		has: function(f,s) { // swapped params in zim 5.3.1
			if (zot(s) || !s.update) s = zimDefaultFrame.stage;
			return zim.Ticker.list && zim.Ticker.list.at(s) && zim.Ticker.list.at(s).indexOf(f) >= 0;
		},
		dispose: function(s) {
			var t = zim.Ticker;
			var count = 0;
			var st;
			for (var i=t.list.length-1; i>=0; i--) { // countdown when removing
				st = t.list.objects[i]; // stage
				if (zot(s) || s === st) {
					t.list.remove(s);
					t.alwaysList.remove(s);
				} else {
					count+=t.list.values[i].length;
				}
			}
			if (t.alwaysList.length > 0) return;
			if (count == 0) {createjs.Ticker.off("tick", t.ticker); t.ticker = null;}
			return true;
		}
	}

	Object.defineProperty(zim.Ticker, 'update', {
		get: function() {
			return zim.Ticker.myUpdate;
		},
		set: function(value) {
			var t =  zim.Ticker;
			if (typeof value != "boolean") value = null;
			t.myUpdate = value;
			if (t.myUpdate === false) {
				 cancelAnimationFrame(t.ticker);
				 // note, this overrides always()
				 // but running always() will override update = false
				 t.alwaysList = new zim.Dictionary();
			}
		}
	});//-30


// SUBSECTION PAGES, LAYOUT, ACCESSIBILITY

/*--
zim.Pages = function(pages, transition, speed, transitionTable, holder, style, group, inherit)

Pages
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Pages handle going between pages.
Make a Pages object and add it to the stage.
All your pages from then on are added to and manipulated inside the Pages object.
Pages allows you to set the destination pages for swipe events.
Other events like buttons can call the go(page, direction) method.
Consider using HotSpots() to efficiently handle multiple buttons.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// make pages (these would be containers with content)
var home = new Rectangle(stageW, stageH, "blue");
var hide = new Rectangle(stageW, stageH, "green");
var find = new Rectangle(stageW, stageH, "yellow");

var pages = new Pages({
	pages:[
		// imagine pages to the left, right, up and down
		// swipe:["to page on left", "to page on right", etc.s]
		{page:home, swipe:[null,"info",hide,find]},
		{page:hide, swipe:[null,null,null,home]},
		{page:find, swipe:[null,null,home,null]}
	],
	transition:"slide",
	speed:1000 // slower than usual for demonstration
});
stage.addChild(pages);

// handle any events inserted into the swipe arrays
pages.on("info", function(){zog("info requested")});

// handle any custom requirements when arriving at a page
// the event gives you the page object
// so add a name properties just make it easier to manage
home.name = "home";
hide.name = "hide";
find.name = "find";
pages.on("page", function() {
	zog(pages.page.name); // now we know which page we are on
})

// you can manually go to pages as well
// we will make a little triangle to click:
var back = new Triangle({color:"red"});
back.center(find); // add triangle to find page
// not really supposed to add things to zim shapes
// they default to mouseChildren false
// we want to click on the back button
// so we have to set the mouseChildren of find to true
find.mouseChildren = true;
back.cursor = "pointer";
back.on("click", function() {pages.go(home, "up")});
stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
pages - (default null) an array of pages or page objects - for example:
	[home, hide] assuming home and hide Containers are made OR if swipe data is desired then use page object format:
	[{page:home, swipe:[null,"info",hide,find]},{page:hide, swipe:[null,null,null,home]}]
	the pages should be containers - it helps to give them each a name property
	the optional swipe array holds mappings to swipe events ["right", "left", "down", "up"]
	in other words, these could be pages to the left, right, top and bottom of the current page
	or they can call commands as strings
transition - (default "none") the type of transition "none", "reveal", "slide", "fade", "clear", "black", "white"
speed - (default 200) speed in milliseconds of the transition if set
transitionTable - (default none) an array to override general transitions with following format:
	[[fromPage, toPage, "transition", ms(optional)], etc.]
holder - (default the default stage) where are we putting the pages (used for setting transition properties)

METHODS
addPage(page, swipeArray) - lets you alternatively add pages after you create the object
removePage(page) - lets you remove a page (if on this page, call a go() first and remove on the page event)
setSwipeArray(page, swipeArray) - lets you set the swipe array for a page
go(newPage, direction, trans, ms) - lets you go to a page for events other than swipe events
	newPage can be a reference to the page or an index matching the pages array order
	direction is which way the pages is relative to the current page
	trans and ms are optional and will override any previously set transitions (speed in ms)
resize() - call to resize transitions - not the pages themselves (use layouts)
pause() - pauses a transition before it starts (call from swipe event)
unpause() - unpauses a paused transition (unless another go() command is called)
puff(time) - adds all the pages behind the currentPage (adding time (ms) will auto calls settle)
settle() - removes all pages except the currentPage
disable() - stops swipe from activating and sets active = false
enable() - enables swipe action and sets active = true
dispose() - clears your listeners and pages

PROPERTIES
type - holds the class name as a String
speed - of transitions in ms
transitionTable - [[fromPage, toPage, "transition", ms(optional)], etc.] overrides default transition
page - the current page object (read)
index - the index of the current page in pages (read)
pages - the page array initially passed as a parameter including any updates if using addPage or removePage methods
lastPage - the last page before transition (read)
direction - direction of transition (read)
transitioning - read only Boolean as to whether the pages are transitioning
active - default true, boolean to have swipes active (good for layered Pages objects)
swipe - the ZIM Swipe object used for pages (can tweak distance to percentage if rescaling)
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
Pages dispatches a "page" event when the page changes (to a page in the swipe array)
myPages.on("page",function(e){...})
with myPages.page being set to the new page (e.target.page)
and myPages.lastPage being set to the old page (e.target.lastPage)
myPages.direction gets the direction of the transition (e.target.direction)

if there is a string in the swipe array like "info"
then the Pages() object dispatches an event equivalent to the string
for the data example above, myPages.on("info",function(e){...});
would trigger when the home page is swiped to the left

Pages dispatches a "swipe" event before changing pages if swiped
you can then get pages.page, pages.nextPage and pages.direction
you can pause() if needed the transition to handle data, etc. and then unpause()
you do not need to handle going to another page when swiping - that is handled automatically
so you probably will not use the swipe event unless handling data between pages

Pages also dispatches a "pagetransitioned" event when a transition is complete
you will have the same properties available as with the page event

USAGE
the first page object is the start page
for the data above, swiping the home page down automatically goes to the hide page
if the home page is swiped up it automatically goes to the find page
you can add pages with the addPage() method
it will not show until you swipe or go to it - unless it was the first page added
1. if the holder is the stage then add the pages object to the stage
2. if the holder is another container then add pages object to the holder
and add the holder to the stage (read this twice to make sure you got it!)
in the second case, you will have to mask the holder so you do not see transitions
DO NOT add the pages to the stage or holder - let Pages do it for you
sometimes you need a page to be on the stage to operate on it
if this is the case, call puff() and make adjustments
call settle() when done - or pass in a time in ms to puff to auto settle after that time
you can define multiple pages objects add and remove pages objects as needed
--*///+71
	zim.Pages = function(pages, transition, speed, transitionTable, holder, style, group, inherit) {
		var sig = "pages, transition, speed, transitionTable, holder, style, group, inherit";
		var duo; if (duo = zob(zim.Pages, arguments, sig, this)) return duo;
		z_d("71");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Pages";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(pages)) pages = DS.pages!=null?DS.pages:[]; // can add pages with addPages
		this.pages = pages;
		if (zot(transition)) transition = DS.transition!=null?DS.transition:"none";
		if (zot(speed)) speed = DS.speed!=null?DS.speed:200;
		if (zot(transitionTable)) transitionTable = DS.transitionTable!=null?DS.transitionTable:[];
		this.transitionTable = transitionTable;

		if (zot(holder)) holder = DS.holder!=null?DS.holder:null;
		if (zot(holder) && zimDefaultFrame) holder = zimDefaultFrame.stage;
		if (!holder.getBounds || !holder.getBounds()) {zog("zim controls - Pages():\nholder object must have bounds set"); return;}

		this.speed = speed;
		this.active = true;
		var that = this;
		that.transitioning = false;

		var hW = holder.getBounds().width;
		var hH = holder.getBounds().height;

		var black; var white;
		if (transition!="none" || transitionTable!=[]) makeTransitionAssets();

		function makeTransitionAssets() {
			black = new createjs.Shape();
			black.graphics.f("black").r(0,0,hW,hH+1);
			white = new createjs.Shape();
			white.graphics.f("white").r(0,0,hW,hH+1);
		}

		var directions = ["left","right","up","down"];

		var data; // holds the page data object
		var page; // holds a page

		for (var i=0; i<pages.length; i++) {
			data = pages[i];
			if (data.constructor !== {}.constructor) data = pages[i] = {page:pages[i]}; // accept an array of only pages
			preparePage(data.page, data.swipe);
		}
		var currentPage = this.page = pages[0] ? pages[0].page : null;
		if (!zot(this.page)) this.index = 0;
		this.addChild(currentPage);
		addHTML(currentPage);

		this.swipe = new zim.Swipe(holder);

		// handle giving swipe event time to trigger event and provide code intervention
		var pauseInfo;
		var paused = false;

		var swipeEvent = this.swipe.on("swipe", function(e) {
			if (!that.active) return;
			var direction = e.currentTarget.direction
			if (direction == "none") return;
			// swap direction (swipe up means move down)
			var newDirection = "";
			if (direction=="left") newDirection="right";
			else if (direction=="right") newDirection="left";
			else if (direction=="up") newDirection="down";
			else if (direction=="down") newDirection="up";
			direction = newDirection;
			var dirIndex = directions.indexOf(direction);
			page = currentPage.zimSwipeArray[dirIndex];

			pauseInfo = [page, direction, null, null, true];
			that.page = currentPage;
			that.nextPage = page;
			that.direction = direction;
			that.dispatchEvent("swipe");

			setTimeout(function() {
				if (!paused) {
					that.go(page, direction, null, null, true); // true is from swipe
				}
			}, 50);
		});

		function preparePage(p, s) {
			// record any TextArea or Loader tags:
			p.zimHTMLList = new zim.Dictionary();
			removeHTML(p);

			p.zimSwipeArray = s ? s : [];
			p.zimOriginalAlpha = p.alpha;
			if (p.parent) p.parent.removeChild(data.page);
		}

		function addHTML(p) {
			if (zot(p)) return;
			for (var i=0; i<p.zimHTMLList.length; i++) {
				p.addChildAt(p.zimHTMLList.values[i].obj, p.zimHTMLList.values[i].depth);
			}
		}

		function removeHTML(p) {
			p.zimHTMLList.clear(); // re-add just in case depth changes or tag is removed
			for (var i=0; i<p.numChildren; i++) { // record depths first
				ch = p.getChildAt(i);
				if (ch.type == "TextArea" || ch.type == "Loader" || ch.type == "Tag") {
					var obj = {obj:ch, depth:p.getChildIndex(ch)};
					p.zimHTMLList.add(ch, obj);
				}
			}
			for (var i=p.numChildren-1; i>=0; i--) { // remove textareas and loaders second
				ch = p.getChildAt(i);
				if (ch.type == "TextArea" || ch.type == "Loader" || ch.type == "Tag") {
					p.removeChild(ch);
				}
			}
		}

		this.addPage = function(page, swipeArray) {
			preparePage(page, swipeArray)
			if (!currentPage) {
				currentPage = that.page = page;
				that.addChild(currentPage);
			} else {
				if (page.parent) page.parent.removeChild(page);
			}
		}

		this.removePage = function(page) {
			// deal with textarea and loader
			removeHTML(page);
			if (that.currentPage == page) {
				that.removeChild(page);
				if (holder.stage) holder.stage.update(); // works even if holder is stage
			}
			page.zimSwipeArray = null;
		}

		this.setSwipeArray = function(page, swipeArray) {
			if (zot(swipeArray)) swipeArray = [];
			var data = {page:page, swipe:swipeArray};
			data.page.zimSwipeArray = (data.swipe) ? data.swipe : [];
		}

		this.pause = function() {
			paused = true;
		}
		this.unpause = function() {
			if (paused) that.go(pauseInfo[0], pauseInfo[1], pauseInfo[2], pauseInfo[3], pauseInfo[4]);
		}

		var goCheck = true;
		this.go = function(newPage, direction, trans, ms, fromSwipe) {

			if (typeof newPage == "number") {
				var np = that.pages[newPage];
				if (zot(np)) return;
				newPage = np.page;
			}

			// newPage holds a page or a string command
			setTimeout(function() {paused = false;},200);
			var slides = [{x:hW},{x:-hW},{y:hH},{y:-hH}];
			var slides2 = [{x:0},{x:0},{y:0},{y:0}];
			var reveals = [{x:hW/2,alpha:0},{x:-hW/2,alpha:0},{y:hH/2,alpha:0},{y:-hH/2,alpha:0}];

			// check for default transition override in transitionTable
			var tempTransition = transition; // default transition
			var tempMs = speed; // default transition speed
			for (var i=0; i<that.transitionTable.length; i++) {
				if (that.transitionTable[i][0]==currentPage && that.transitionTable[i][1]==newPage) {
					tempTransition = that.transitionTable[i][2];
					tempMs = that.transitionTable[i][3];
					break;
				}
			}
			// transition passed into go overrides all transitions
			// so if there is not a transition parameter set trans tempTransition
			// which is either the transition table transition or the default
			if (zot(trans)) trans = tempTransition; // use default
			if (zot(ms)) ms = tempMs; // use default
			that.speed = ms;

			that.direction = direction;
			if (newPage=="" || newPage==null) {
				that.page = currentPage;
				that.dispatchEvent("nothing");
			} else if (typeof newPage === 'string') {
				that.page = currentPage;
				that.dispatchEvent(newPage);
			} else {
				if (newPage == currentPage) return; // same page ;-)
				if (zot(direction)) direction="right";
				var dirIndex = directions.indexOf(direction);

				if (!goCheck) return;
				goCheck = false;

				function transEnd(pages) {
					pages[0].uncache();
					pages[1].uncache();
					that.transitioning = false;
					that.dispatchEvent("pagetransitioned");
					that.removeChild(that.lastPage);
					that.removeChild(black);
					that.removeChild(white);
					addHTML(currentPage);
					goCheck = true;
				}

				function transEndHalf(pages) {
					that.removeChild(that.lastPage);
					zim.animate(pages.shift(), {alpha:0}, that.speed/2, null, transEnd, pages);
				}

				newPage.x = 0;
				newPage.y = 0;
				newPage.alpha = newPage.zimOriginalAlpha;

				that.transitioning = true;
				if (trans == "slide") {
					newPage.x = -(slides[dirIndex].x | 0);
					newPage.y = -(slides[dirIndex].y | 0);
					newPage.cache(0,0,(hW+1)/newPage.scaleX,(hH+1)/newPage.scaleY);
					currentPage.cache(0,0,(hW+1)/currentPage.scaleX,(hH+1)/currentPage.scaleY);
					that.addChild(newPage);
					that.addChild(currentPage);
					zim.animate(currentPage, slides[dirIndex], that.speed, null, transEnd, [currentPage, newPage]);
					zim.animate(newPage, slides2[dirIndex], that.speed);
				} else if (trans == "reveal") {
					newPage.cache(0,0,(hW+1)/newPage.scaleX,(hH+1)/newPage.scaleY);
					currentPage.cache(0,0,(hW+1)/currentPage.scaleX,(hH+1)/currentPage.scaleY);
					that.addChild(newPage); // put destination under current page
					that.addChild(currentPage);
					zim.animate(currentPage, reveals[dirIndex], that.speed, null, transEnd, [currentPage, newPage]);
				} else if (trans == "fade") {
					newPage.cache(0,0,(hW+1)/newPage.scaleX,(hH+1)/newPage.scaleY);
					currentPage.cache(0,0,(hW+1)/currentPage.scaleX,(hH+1)/currentPage.scaleY);
					newPage.alpha = 1;
					that.addChild(newPage);
					that.addChild(currentPage);
					zim.animate(currentPage, {alpha:0}, that.speed, null, transEnd, [currentPage, newPage]);
				} else if (trans == "black") {
					newPage.cache(0,0,(hW+1)/newPage.scaleX,(hH+1)/newPage.scaleY);
					currentPage.cache(0,0,(hW+1)/currentPage.scaleX,(hH+1)/currentPage.scaleY);
					newPage.alpha = 1;
					that.addChild(newPage);
					that.addChild(currentPage);
					black.alpha = 0;
					that.addChild(black);
					zim.animate(black, {alpha:1}, that.speed/2, null, transEndHalf, [black, currentPage, newPage]);
				} else if (trans == "clear") {
					newPage.cache(0,0,(hW+1)/newPage.scaleX,(hH+1)/newPage.scaleY);
					currentPage.cache(0,0,(hW+1)/currentPage.scaleX,(hH+1)/currentPage.scaleY);
					newPage.alpha = 0;
					that.addChild(newPage);
					that.addChild(currentPage);
					zim.animate(currentPage, {alpha:0}, that.speed/2);
					zim.animate(newPage, {alpha:1}, that.speed/2, null, transEnd, [currentPage, newPage], that.speed/2);
				} else if (trans == "white") {
					newPage.cache(0,0,(hW+1)/newPage.scaleX,(hH+1)/newPage.scaleY);
					currentPage.cache(0,0,(hW+1)/currentPage.scaleX,(hH+1)/currentPage.scaleY);
					newPage.alpha = 1;
					that.addChild(newPage);
					that.addChild(currentPage);
					white.alpha = 0;
					that.addChild(white);
					zim.animate(white, {alpha:1}, that.speed/2, null, transEndHalf, [white, currentPage, newPage]);
				} else {
					that.transitioning = false;
					that.addChild(newPage);
					that.removeChild(currentPage);
					goCheck = true;
					// that.dispatchEvent("pagetransitioned"); // hmmm... no
				}

				removeHTML(currentPage);
				that.lastPage = currentPage;
				that.page = newPage;
				that.index = -1;
				for (var i=0; i<that.pages.length; i++) {
					var p = that.pages[i].page;
					if (p == that.page) {
						that.index = i;
					}
				}
				if (trans == "none") addHTML(newPage);
				if (zot(fromSwipe)) fromSwipe = false;
				that.fromSwipe = fromSwipe;
				that.dispatchEvent("page");
				currentPage = newPage;
				if (holder.stage) holder.stage.update();
			}
		}

		this.resize = function() {
			hW = holder.getBounds().width;
			hH = holder.getBounds().height;
			if (transition!="none" || transitionTable!=[]) makeTransitionAssets();
		}

		this.puff = function(milliseconds) {
			// add all pages to the holder behind current page
			// if milliseconds then this is the time to settle automatically
			for (var i=0; i<pages.length; i++) {
				that.addChild(pages[i].page);
			}
			that.addChild(currentPage);
			if (milliseconds > 0) {
				setTimeout(function() {
					that.settle();
				}, milliseconds);
			}
		}

		this.settle = function() {
			that.removeAllChildren();
			that.addChild(currentPage);
			that.dispatchEvent("puffed");
		}

		this.disable = function() {
			that.active = false;
		}

		this.enable = function() {
			that.active = true;
		}

		if (style!==false) zimStyleTransforms(this, DS);

		this.dispose = function() {
			that.swipe.off("swipe", swipeEvent);
			this.zimContainer_dispose();
			return true;
		}

	}
	zim.extend(zim.Pages, zim.Container, ["clone", "dispose"], "zimContainer", false);
	//-71

/*--
zim.HotSpots = function(spots, local, mouseDowns)

HotSpots
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
HotSpots allow you to create multiple hotSpot objects on multiple pages.
A hotSpot is an invisible click area (like an image map in HTML).
You can alternatively specify an object and it will turn that into a hotSpot.
HotSpots lets you control many or all of your interactions in one place.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// our first hotSpot will be a 50 pixel square at 100, 100
// then we will add hotSpots to these items as well
var circle = new Circle(60, "red");
circle.center(stage);

var button = new Button();
stage.addChild(button);
button.x = stageW - button.width - 100;
button.y = stageH - button.height - 100;

// make the hotSpots object
// these are all on the same page
// gets really handy when you have multiple pages with Pages
var hs = new HotSpots([
	{page:stage, rect:[100,100,50,50], call:function(){zog("hot!");}},
	{page:stage, rect:circle, call:function(){zog("circle!");}},
	{page:stage, rect:button, call:function(){zog("button!");}},
]);
// hs.show(); // uncomment this to see rectangle hotSpots
stage.update();
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
spots - an array of hotspot data objects with the following format:
	[{page:home, rect:[190,50,260,260], talk:"screen reader content", call:someFunction, callOver:optionalFunction, callOut:optionalFunction},
	 {page:home, rect:[70,405,500,150], talk:"screen reader content", call:someOtherFunction, callOver:optionalFunction, callOut:optionalFunction}]
	the page should be a Container
	the rect is the [left, right, width, height] of a rectangle relative to the stage
	talk (optional) will provide text for screen reader if ZIM Accessibility() is turned on
	call is the callback function to call when a hotSpot is clicked
	callOver and callOut will add mouseover and mouseout event functions to the hotSpot
	instead of a rect array you can pass an object that must have setBounds() set
	[{page:home, rect:submitButton, call:function(){//code}}]
	the hotSpot will then use the button position and bounds as the rectangle
	note - in this case, HotSpots will actually add a mousedown or click event to the button
local (default true) hotSpot rect is based on local coordinates of the container
	use when the element scale independently from the stage
	if set to false then you pass in global coordinates and hotSpot will convert them
mouseDowns (default false) stops mousedown events on a button that is used as a hotSpot
	prevents users from activating a swipe on a button (when using ZIM Swipe)

METHODS
show() - shows the hotspots for testing during authoring time
hide() - hides the hotspots
addHotSpot(page,rect,call) - can dynamically add hotSpots
removeHotSpots(page,rect) - rect is optional - so can remove all spots on a page or by the rect
dispose() - removes listeners

PROPERTIES
type - holds the class name as a String
hotSpots - an array of HotSpot objects

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)

NOTE: the class does actually add rectangle shapes to your page
the spot is a pixel rect with an alpha of .01 and then uses a hitArea of a backing shape
this could have been done with "math" alone but rollover cursor would be a pain
the class creates HotSpot objects - see the class underneath this one
--*///+72
	zim.HotSpots = function(spots, local, mouseDowns) {
		var sig = "spots, local, mouseDowns";
		var duo; if (duo = zob(zim.HotSpots, arguments, sig, this)) return duo;
		z_d("72");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "HotSpots";

		if (zot(spots) || !Array.isArray(spots)) {zog("zim controls - HotSpots():\nplease provide an array of HotSpot data"); return;}
		if (zot(local)) local = true;
		if (zot(mouseDowns)) mouseDowns = false;
		var eventType = (!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click";

		var that = this;

		var data; // spot data object
		var hs; // hotSpot object
		var hotSpots = that.hotSpots = []; // array of hotSpot objects

		// loop through data and add hotSpot objects
		for (var i=0; i<spots.length; i++) {
			addSpot(spots[i]);
		}

		function addSpot(data) {
			var button = null;
			if (!Array.isArray(data.rect)) {
				button = data.rect; // data includes a button rather than rect
				if (!button) {
					zog("zim controls - HotSpots(): HotSpot "+ data.page + " " + data.rect +" button does not exist");
					return;
				}
				data.rect = [button.x, button.y, button.width, button.height];	// bounds are not used for button
			}
			hs = new zim.HotSpot(data.page,data.rect[0],data.rect[1],data.rect[2],data.rect[3],data.call,data.callOver,data.callOut,local,data.talk);
			hs.zimHSpage = data.page;
			hs.button = button;
			hotSpots.push(hs);
			hs.on(eventType, hsEvent);
			if (button && zot(data.callOver) && zot(data.callOut)) {

				// stop hotSpot from taking away rollovers on button
				hs.spot.mouseEnabled = false;
				hs.spot.mouseChildren = false;
				// but now need to add click to button as hotSpot will not work
				button.zimHScall = data.call;
				button.zimHSEvent = button.on(eventType, hsEvent, true);
				if (!mouseDowns) {
					button.zimHSMDEvent = button.on("mousedown",function(e) {
						e.stopImmediatePropagation();
					});
				}
				button.cursor = "pointer";
			}
		}

		function hsEvent(e) {
			if (e.stopImmediatePropagation) e.stopImmediatePropagation();
			if (window.event) window.event.cancelBubble=true;
			if (typeof(e.currentTarget.zimHScall) == "function") {
				e.currentTarget.zimHScall(e);
			}
		}

		this.addHotSpot = function(page,rect,call,callOver,callOut,talk) {
			data = {page:page, rect:rect, call:call, callOver:callOver, callOut:callOut, talk:talk};
			spots.push(data);
			addSpot(data);
		}

		this.show = function() {
			for (var i=0; i<hotSpots.length; i++) {
				hs = hotSpots[i];
				if (!hs.button) hs.spot.alpha = .2;
			}
		}
		this.hide = function() {
			for (var i=0; i<hotSpots.length; i++) {
				hs = hotSpots[i];
				hs.spot.alpha = 0;
			}
		}

		this.removeHotSpots = function(page, rect) {
			for (var i=spots.length-1; i>=0; i--) {
				data = spots[i];
				hs = hotSpots[i];
				if (rect && !Array.isArray(rect)) { // button
					rect = [rect.x, rect.y, rect.getBounds().width, rect.getBounds().height];
				}
				if (
					(zot(page) && zot(rect)) ||
					(zot(rect) && page==data.page) ||
					(zot(page) && zim.arraysEqual(rect,data.rect)) ||
					(page==data.page && zim.arraysEqual(rect,data.rect))
				) {
					// remove hotSpot from data and hotSpots list
					spots.splice(i,1);
					if (hs.button) {
						hs.button.off(eventType, hs.button.zimHSEvent);
						hs.button.zimHSEvent = null;
						if (!mouseDowns) {
							hs.button.off("mousedown", hs.button.zimHSMDEvent);
							hs.button.zimHSMDEvent = null;
						}
					}
					hs.off(eventType, hsEvent);
					hs.dispose();
					hotSpots.splice(i,1);
				}
			}
		}

		this.dispose = function() {
			for (var i=0; i<hotSpots.length; i++) {
				hs = hotSpots[i];
				if (hs.button) {
					hs.button.off(eventType, hs.button.zimHSEvent);
					hs.button.zimHSCall = null;
					hs.button.zimHSEvent = null;
					if (!mouseDowns) {
						hs.button.off("mousedown", hs.button.zimHSMDEvent);
						hs.button.zimHSMDEvent = null;
					}
				}
				hs.off(eventType, hsEvent);
				hs.dispose();
			}
			return true;
		}
	}
	zim.extend(zim.HotSpots, zim.Container, "clone", "zimContainer", false);
	//-72


/*--
zim.HotSpot = function(obj, x, y, width, height, call, callOver, callOut, local, talk)

HotSpot
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
HotSpot adds an invisible button to a container object (often think of this as the page).
If you want multiple spots it is more efficient to use the HotSpots class above
which manages multiple HotSpot objects (otherwise you end up with multiple event functions).
The spot is a pixel rect with an alpha of .01 and then uses a hitArea of a backing shape.
The spot will get a cursor of "pointer".

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var hs = new HotSpot(stage, 100, 100, 50, 50, myFunction);
function myFunction() {
	zog("activation!");
}
// hs.show(); // uncomment this to see rectangle hotSpot
stage.update();
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
holder - container object in which to place the hotspot (stage for instance)
x, y, width and height - of the rectangle for the hotspot
call - the function to call when the spot is pressed
local - (default true) hotSpot rect is based on local coordinates of the container
	use when the element scale independently from the stage
	if set to false then you pass in global coordinates and hotSpot will convert them
talk - (default "hotspot") text for ZIM Accessibility screen reader

METHODS
show() - helps when creating the spot to see where it is
hide() - hides the hotspot
dispose() - removes the listener and the spot

PROPERTIES
type - holds the class name as a String
spot - the actual hotSpot object that gets added to the container can be accessed with the spot property
eg. hs.spot

ACTIONEVENT
This component is affected by the general ACTIONEVENT setting
The default is "mousedown" - if set to something else the component will act on click (press)
--*///+73
	zim.HotSpot = function(obj, x, y, width, height, call, callOver, callOut, local, talk) {
		var sig = "obj, x, y, width, height, call, callOver, callOut, local, talk";
		var duo; if (duo = zob(zim.HotSpot, arguments, sig, this)) return duo;
		z_d("73");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "HotSpot";

		if (zot(obj) || !obj.addChild) {zog("zim controls - HotSpot():\nPlease pass in container object for obj"); return;}
		if (obj instanceof createjs.Container == false) {zog("zim controls - HotSpot():\nObjects passed in should be Containers"); return;}
		if (zot(x) || zot(y) || zot(width) || zot(height)) {zog("zim controls - HotSpot():\nPlease pass in x, y, width, height"); return;}
		if (zot(local)) local = true;
		eventType = (!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click";

		var w = width; var h = height;
		var that = this;

		if (!local) {
			var point = obj.globalToLocal(x,y);
			var point2 = obj.globalToLocal(x+w,y+h);
			var newW = point2.x-point.x;
			var newH = point2.y-point.y;
		} else {
			var point = new zim.Point(x,y);
			var newW = width;
			var newH = height;
		}
		var but = new zim.Shape(width, height);
		but.talk = zot(talk)?"HotSpot":talk;
		but.alpha = 0;
		but.graphics.f("black").dr(0,0,newW,newH);
		but.x = point.x;
		but.y = point.y;
		but.cursor = "pointer";
		but.expand(0);
		this.spot = but;

		if (typeof(call) == "function") {
			var butEvent = but.on(eventType,function(e) {
				call(e);
			});
		}
		if (typeof(callOver) == "function") {
			var butOverEvent = but.on("mouseover",function(e) {
				callOver(e);
			});
		}
		if (typeof(callOut) == "function") {
			var butOutEvent = but.on("mouseout",function(e) {
				callOut(e);
			});
		}
		obj.addChild(but);

		this.show = function() {
			obj.addChild(backing);
		}
		this.hide = function() {
			obj.removeChild(backing);
		}
		this.dispose = function() {
			if (butEvent) but.off(eventType, butOverEvent);
			if (butOverEvent) but.off("mouseover", butOverEvent);
			if (butOutEvent) but.off("mouseout", butOutEvent);
			obj.removeChild(but);
			but = null;
			return true;
		}
	}
	zim.extend(zim.HotSpot, zim.Container, "clone", "zimContainer", false);
	//-73

/*--
zim.Guide = function(obj, vertical, percent, hideKey, pixelKey, style, group, inherit)

Guide Class
extends a zim.Container which extends a createjs.Container

DESCRIPTION
Guide shows a guideline to help layout assets with code.
Cursor x and y in percentage or pixels are shown along edges
as a distance from the guide.
You only need one guide per axis because you measure from the guide to your cursor.
Use the G key to toggle guide visibility.
Use the P key to toggle percent and pixels.
Make sure you remove the guide for your final version (dispose).

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// simple form for a vertical guide
// use the distance from the guide to your cursor to measure
// so you only need one vertical guide for horizontal measurement
var guide = new Guide();

// better to add guides to a GuideManager
var manager = new GuideManager();
manager.add(new Guide(stage));
manager.add(new Guide(stage, false));

// or with pixels
// manager.add(new Guide(stage, true, false));
// manager.add(new Guide(stage, false, false));

// then you can remove all guides with
// manager.dispose();
// handy with guides on multiple Pages

// and in frame resize event we can resize all guides:
frame.on("resize", function() {manager.resize();})
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
obj - (default the default stage) object to add guide to for example a Container
vertical - (default true) set to false for horizontal guide
percent - (default true) set to false to show pixels
hideKey - (default G) key to press to hide guide
pixelKey - (default P) key to press to swap percent and pixels

METHODS
resize() - resizes the guide if the container size changes (put in frame resize event)
toggle(state - default null) - toggle() will show controls if they are hidden or hide controls if they are showing
	alternatively, pass in true to show controls or false to hide controls
	note - method does not update the stage
dispose() - removes keyboard event listeners and guide

PROPERTIES
type - holds the class name as a String
toggled - boolean - read only true if controls are showing or false if controls are hidden
pixels - boolean - set to true to change to pixels, false to go to percent
--*///+76
	zim.Guide = function(obj, vertical, percent, hideKey, pixelKey, style, group, inherit) {
		var sig = "obj, vertical, percent, hideKey, pixelKey, style, group, inherit";
		var duo; if (duo = zob(zim.Guide, arguments, sig, this)) return duo;
		z_d("76");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Guide";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(obj)) size = DS.obj!=null?DS.obj:null;
		if (zot(obj)) {
			if (zimDefaultFrame) {
				obj = zimDefaultFrame.stage;
			} else {
				obj = "stage";
			}
		}
		if (zot(vertical)) vertical = DS.vertical!=null?DS.vertical:true;
		if (obj != "stage" && (!obj.addChild || !obj.getBounds || !obj.getBounds())) {zog ("zim controls - Guide(): Please provide container with bounds for the obj (setBounds())"); return;}
		if (zot(percent)) percent = DS.percent!=null?DS.percent:true;
		if (zot(hideKey)) hideKey = DS.hideKey!=null?DS.hideKey:"G";
		if (zot(pixelKey)) pixelKey = DS.pixelKey!=null?DS.pixelKey:"P";

		var that = this;
		var stageEvent;

		// make text boxes that show x and y
		var boxW = 80;
		var boxH = 26;
		var minX = boxW/6+boxW/2;
		var minY = boxH*2
		var maxX; // set max values once we get a stage
		var maxY;

		var box;
		if (vertical) {
			box = makeBox("#00c5af", "white", "white");
			box.shape.regX = boxW+boxW/6; box.shape.regY = boxH/2;
			box.label.x = -boxW/2-boxW/6;
		} else {
			box = makeBox("#d61fa0", "white", "white");
			box.shape.regX = boxW/2; box.shape.regY = boxH + boxH/4;
			box.label.y = -boxH*3/4;
		}

		function makeBox(fill, stroke, color) {
			var box = new zim.Container({style:false});
			box.shape = new createjs.Shape();
			box.shape.graphics.s(stroke).ss(1).f(fill).r(0,0,boxW,boxH);
			box.shape.alpha = .9;
			box.addChild(box.shape);
			box.label = new createjs.Text("10", "16px arial", color);
			box.label.textAlign = "center";
			box.label.textBaseline = "middle";
			box.addChild(box.label);
			box.mouseChildren = false;
			box.mouseEnabled = false;
			return box;
		}

		var stage;
		if (obj != "stage") obj.addChild(that);
		var addedInterval = zim.added(that, added);

		var guideCheck = false;
		var objW;
		var objH;
		var line;
		var dragBounds;
		function added() {
			if (obj == "stage") {
				stage =	that.stage;
				obj = stage;
			} else {
				stage =	obj.stage;
			}
			obj.addChild(that);
			objW = obj.getBounds().width;
			objH = obj.getBounds().height;
			if (vertical) {
				box.y = objH/2;
				box.label.text = "x:" + ((!that.percent) ? Math.round(objW*70/100) : "70%");
			} else {
				box.x = objW/2;
				box.label.text = "y:" + ((!that.percent) ? Math.round(objH*70/100) : "70%");
			}
			line = new createjs.Shape();
			that.addChild(line);
			(vertical) ? line.x = objW*.7 : line.y = objH*.7;

			if (!guideCheck) {
				obj.addChild(that);
				drawGuide();
			}
			stage.off("stagemousemove", stageEvent);
			stageEvent = stage.on("stagemousemove", where);
			stage.update();
		};

		var lastPoint = {x:0,y:0};
		function where(e) {
			// convert mouse location to local point
			var point; var diff;
			if (e) {
				point = obj.globalToLocal(e.rawX/stage.scaleX, e.rawY/stage.scaleY);
				lastPoint = point;
			} else {
				point = {x:lastPoint.x, y:lastPoint.y}
			}
			if (!percent) {	// pixels
				if (vertical) {
					diff = Math.round(Math.abs(point.x-line.x));
					box.label.text = "x:" + Math.max(0, Math.min(diff, Math.round(objW)));
					box.y = Math.max(minY, Math.min(point.y, maxY));
				} else {
					diff = Math.round(Math.abs(point.y-line.y));
					box.label.text = "y:" + Math.max(0, Math.min(diff, Math.round(objH)));
					box.x = Math.max(minX, Math.min(point.x, maxX));
				}
			} else {
				if (vertical) {
					diff = Math.round(Math.abs(point.x-line.x)/objW*100);
					box.label.text = "x:" + Math.max(0, Math.min(diff, 100)) + "%";
					box.y = Math.max(minY, Math.min(point.y, maxY));
				} else {
					diff = Math.round(Math.abs(point.y-line.y)/objH*100);
					box.label.text = "y:" + Math.max(0, Math.min(diff, 100)) + "%";
					box.x = Math.max(minX, Math.min(point.x, maxX));
				}
			}
			if (stage) stage.update();
		}

		// make the guide once we have the stage
		// and any time resize is called
		function drawGuide() {
			guideCheck = true;
			objW = obj.getBounds().width;
			objH = obj.getBounds().height;
			var pointer;
			if (vertical) {
				pointer = "ew-resize";
				dragBounds = new createjs.Rectangle(0,0,objW,0);
			} else {
				pointer = "ns-resize";
				dragBounds = new createjs.Rectangle(0,0,0,objH);
			}
			zim.noDrag(line);
			setTimeout(function() {
				// give time for content to settle
				zim.drag(line, dragBounds, pointer, pointer, null, null, true);
			}, 500);
			stage.mouseMoveOutside = true;
			stage.enableMouseOver(10);

			maxX = objW-boxW*2/3;
			maxY = objH-boxH - boxH;
			line.uncache();
			var g = line.graphics;
			if (vertical) {
				g.c().s("rgba(0,255,255,.1)").ss(20).mt(0,0).lt(0,objH);
				g.f().s("white").ss(2).mt(0,0).lt(0,objH);
				g.s("#00c5af").sd([20,20]).mt(0,0).lt(0,objH).sd();
				line.cache(-10,0,20,objH);
			} else {
				g.c().s("rgba(255,0,255,.1)").ss(20).mt(0,0).lt(objW,0);
				g.f().s("white").ss(2).mt(0,0).lt(objW, 0);
				g.s("#d61fa0").sd([20,20]).mt(0,0).lt(objW, 0).sd();

				line.cache(0,-10,objW,20);
			}

			(vertical) ?  box.x = objW : box.y = objH;
			that.addChild(box);

		}

		Object.defineProperty(this, 'pixels', {
			get: function() {
				return !percent;
			},
			set: function(value) {
				percent = !value;
				that.resize();
			}
		});

		// add key listener to hide and show the guide
		window.addEventListener("keydown", keyEvent);

		function keyEvent(e) {
			if (!e) e=event;
			if (!stage) return;
			if (String.fromCharCode(e.keyCode) == hideKey.toUpperCase()) { // G
				that.toggle();
			}
			if (String.fromCharCode(e.keyCode) == pixelKey.toUpperCase()) { // P
				that.pixels = !that.pixels;
			}
		}

		this.toggled = true;
		this.toggle = function(type) {
			if (type === true) {
				that.visible = that.toggled = true;
			} else if (type === false) {
				that.visible = that.toggled = false;
			} else {
				that.visible = !that.visible;
				that.toggled = that.visible;
			}
			stage.off("stagemousemove", stageEvent);
			if (that.visible) {
				stageEvent = stage.on("stagemousemove", where, that);
			}
			return that;
		}

		this.resize = function() {
			if (!that) return false;
			if (stage) {
				drawGuide();
				where();
			}
			return true;
		}

		if (style!==false) zimStyleTransforms(this, DS);

		this.dispose = function() {
			if (!that) return false;
			zim.noDrag(line);
			clearInterval(addedInterval);
			that.removeAllChildren();
			window.removeEventListener("keydown", keyEvent);
			if (that.parent) that.parent.removeChild(that);
			that = null;
			return true;
		}
	}
	zim.extend(zim.Guide, zim.Container, "clone", "zimContainer", false);
	//-76

/*--
zim.Grid = function(obj, color, percent, hideKey, pixelKey, style, group, inherit)

Grid
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A Grid shows gridlines to help layout assets with code (percent is default).
Cursor x and y percentage or pixels are shown along edges.
Use the G key to toggle grid visibility.
Use the P key to toggle percent and pixels.
Make sure you remove the grid for your final version (dispose).

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var grid = new Grid();

// better to add grids to a GridManager
var manager = new GridManager();
manager.add(new Grid());

// or with pixels
// manager.add(new Grid(null, false));

// then you can remove all grids with
// grid.dispose();
// handy with guides on multiple Pages

// and in frame resize event we can resize all grids:
frame.on("resize", function() {manager.resize();})
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
obj - (default the default stage) the object to add grid to (for example a Container)
color - (default black) the color of the grid
percent - (default true) set to false to show pixels
hideKey - (default G) key to press to hide grid
pixelKey - (default P) key to press to swap percent and pixels

METHODS
resize() - resize the grid if the container changes size (eg. put in frame resize event)
toggle(state - default null) - toggle() will show controls if they are hidden or hide controls if they are showing
	alternatively, pass in true to show controls or false to hide controls
	note - method does not update the stage
	returns object for chaining
dispose() - clears keyboard events and grid

PROPERTIES
type - holds the class name as a String
toggled - boolean - read only true if controls are showing or false if controls are hidden
pixels - boolean - set to true to change to pixels, false to go to percent
--*///+78
	zim.Grid = function(obj, color, percent, hideKey, pixelKey, style, group, inherit) {
		var sig = "obj, color, percent, hideKey, pixelKey, style, group, inherit";
		var duo; if (duo = zob(zim.Grid, arguments, sig, this)) return duo;
		z_d("78");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Grid";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(obj)) size = DS.obj!=null?DS.obj:null;
		if (zot(obj)) {
			if (zimDefaultFrame) {
				obj = zimDefaultFrame.stage;
			} else {
				obj = "stage";
			}
		}
		if (zot(color)) color = DS.color!=null?DS.color:"black";
		if (obj != "stage" && (!obj.addChild || !obj.getBounds || !obj.getBounds())) {zog ("zim controls - Grid(): Please provide container with bounds for the obj (setBounds())"); return;}
		if (zot(percent)) percent = DS.percent!=null?DS.percent:true;
		if (zot(hideKey)) hideKey = DS.hideKey!=null?DS.hideKey:"G";
		if (zot(pixelKey)) pixelKey = DS.pixelKey!=null?DS.pixelKey:"P";

		var that = this;
		var pixels = 10; // for grid
		var stageEvent;


		this.mouseChildren = false;
		this.mouseEnabled = false;

		// make text boxes that show x and y
		var boxW = 80;
		var boxH = 26;

		var top = makeBox("#dddddd", color, "#333333");
		top.shape.regX = boxW/2; top.shape.regY = -boxH/4;
		top.label.y = boxH*3/4;

		var left = makeBox("#dddddd", color, "#333333");
		left.shape.regX = -boxW/6; left.shape.regY = boxH/2;
		left.label.x = boxW/2 + boxW/6;

		function makeBox(fill, stroke, color) {
			var box = new zim.Container({style:false});
			box.shape = new createjs.Shape();
			box.shape.graphics.s(stroke).ss(1).f(fill).r(0,0,boxW,boxH);
			box.shape.alpha = .9;
			box.addChild(box.shape);
			box.label = new createjs.Text("10", "16px arial", color);
			box.label.textAlign = "center";
			box.label.textBaseline = "middle";
			box.addChild(box.label);
			box.mouseChildren = false;
			box.mouseEnabled = false;
			return box;
		}

		var minX = boxW/6+boxW/2;
		var minY = boxH*2
		var maxX; // set max values once we get a stage
		var maxY;

		top.x = minX;
		left.y = minY;
		top.label.text = "x:0";
		left.label.text = "y:0";

		var stage;
		if (obj != "stage") obj.addChild(that);
		var addedInterval = zim.added(that, added);

		var gridCheck = false;
		function added() {
			if (obj == "stage") {
				stage =	that.stage;
				obj = stage;
			} else {
				stage =	obj.stage;
			}
			if (!gridCheck) {
				drawGrid();
				obj.addChild(that);
			}
			stage.off("stagemousemove", stageEvent);
			stageEvent = stage.on("stagemousemove", where);
			stage.update();
		};
		var lastPoint = {x:0,y:0};
		function where(e) {
			// convert mouse location to local point
			var point;
			if (e) {
				point = obj.globalToLocal(e.rawX/stage.scaleX, e.rawY/stage.scaleY);
				lastPoint = point;
			} else {
				point = {x:lastPoint.x, y:lastPoint.y}
			}
			if (!percent) {	// pixels
				top.label.text = "x:" + Math.max(0, Math.min(Math.round(point.x), Math.round(objW)));
				top.x = Math.max(minX, Math.min(point.x, maxX));
				left.label.text = "y:" + Math.max(0, Math.min(Math.round(point.y), Math.round(objH)));
				left.y = Math.max(minY, Math.min(point.y, maxY));
			} else {
				top.label.text = "x:" + Math.max(0, Math.min(Math.round(point.x/objW*100), 100)) + "%";
				top.x = Math.max(minX, Math.min(point.x, maxX));
				left.label.text = "y:" + Math.max(0, Math.min(Math.round(point.y/objH*100), 100)) + "%";
				left.y = Math.max(minY, Math.min(point.y, maxY));
			}
			if (stage) stage.update();
		}

		// make the grid once we have the stage
		var objW;
		var objH;
		var cached;
		function drawGrid() {
			 gridCheck = true;

			if (obj && obj.getBounds) {
				objW = obj.getBounds().width;
				objH = obj.getBounds().height;
			}
			if (stage) {
				stage.mouseMoveOutside = true;
				stage.enableMouseOver(10);
			}

			maxX = objW-boxW*2/3;
			maxY = objH-boxH - boxH;

			cached = new zim.Container({style:false});
			that.addChild(cached);
			var grid = new createjs.Shape();
			cached.addChild(grid);
			var g = grid.graphics;
			g.s(color).ss(1);

			var grid2 = new createjs.Shape();
			cached.addChild(grid2);


			if (!percent) { // pixels

				for (var i=0; i<objW/pixels; i++) {
					g.mt(i*pixels, 0).lt(i*pixels, objH);
				}
				for (var i=0; i<objH/pixels; i++) {
					g.mt(0, i*pixels).lt(objW, i*pixels);
				}
				grid.alpha = .3;

				g = grid2.graphics;
				g.s(color).ss(1);

				for (var i=0; i<objW/(pixels*10); i++) {
					g.mt(i*(pixels*10), 0).lt(i*(pixels*10), objH);
				}
				for (var i=0; i<objH/(pixels*10); i++) {
					g.mt(0, i*(pixels*10)).lt(objW, i*(pixels*10));
				}

			} else { // percent - every 5 percent

				for (var i=1; i<20+2; i++) {
					g.mt(i*objW/20, 0).lt(i*objW/20, objH);
				}
				for (var i=1; i<20; i++) {
					g.mt(0, i*objH/20).lt(objW, i*objH/20);
				}
				grid.alpha = .3;

				g = grid2.graphics;
				g.s(color).ss(1);

				for (var i=1; i<10; i++) {
					g.mt(i*objW/10, 0).lt(i*objW/10, objH);
				}
				for (var i=1; i<10; i++) {
					g.mt(0, i*objH/10).lt(objW, i*objH/10);
				}
			}

			var crossSize = 80;
			g.s("#FFFFFF").ss(8);
			g.mt(objW/2, objH/2-crossSize/2).lt(objW/2, objH/2+crossSize/2);
			g.mt(objW/2-crossSize/2, objH/2).lt(objW/2+crossSize/2, objH/2);

			g.s("#000000").ss(4);
			g.mt(objW/2, objH/2-crossSize/2).lt(objW/2, objH/2+crossSize/2);
			g.mt(objW/2-crossSize/2, objH/2).lt(objW/2+crossSize/2, objH/2);

			// draw a border
			g.s(color).ss(3);
			g.dr(0,0,objW,objH);

			grid2.alpha = .5;
			cached.cache(0,0,objW,objH);

			that.addChild(top);
			that.addChild(left);

		 	if (stage) stage.update();
		}

		Object.defineProperty(this, 'pixels', {
			get: function() {
				return !percent;
			},
			set: function(value) {
				percent = !value;
				that.resize();
			}
		});

		// add key listener to hide and show the grid
		window.addEventListener("keydown", keyEvent);

		function keyEvent(e) {
			if (!e) e=event;
			if (!stage) return;
			if (String.fromCharCode(e.keyCode) == hideKey.toUpperCase()) { // G
				that.toggle();
			}
			if (String.fromCharCode(e.keyCode) == pixelKey.toUpperCase()) { // P
				that.removeChild(cached);
				cached = null;
				that.pixels = !that.pixels;
			}
		}

		this.toggled = true;
		this.toggle = function(type) {
			if (type === true) {
				that.visible = that.toggled = true;
			} else if (type === false) {
				that.visible = that.toggled = false;
			} else {
				that.visible = !that.visible;
				that.toggled = that.visible;
			}
			stage.off("stagemousemove", stageEvent);
			if (that.visible) {
				stageEvent = stage.on("stagemousemove", where, that);
			}
			return that;
		}

		this.resize = function() {
			if (!that) return false;
			that.removeChild(cached);
			cached = null;
			if (stage) {
				drawGrid();
				where();
				setTimeout(function(){ // solve ipod bug
					that.removeChild(cached);
					cached = null;
					drawGrid();
				},200);
			}
			return true;
		}

		if (style!==false) zimStyleTransforms(this, DS);

		this.dispose = function() {
			clearInterval(addedInterval);
			that.removeAllChildren();
			window.removeEventListener("keydown", keyEvent);
			if (that.parent) that.parent.removeChild(that);
			that = null;
			return true;
		}

	}
	zim.extend(zim.Grid, zim.Container, "clone", "zimContainer", false);
	//-78

/*--
zim.Tile = function(obj, cols, rows, spacingH, spacingV, width, height, squeezeH, squeezeV, colSize, rowSize, align, valign, count, mirrorH, mirrorV, snapToPixel, clone, group, style, inherit)

Tile
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
Tile has two main uses:

1. ART
Creates a tile with an object for the columns and rows specified - a mirror effect is available too.
By default the object is cloned.
A ZIM VEE value can be passed to tile multiple types of objects

2. LAYOUT
Pass in a series(obj1, obj2, obj3, etc.) to tile unique objects
These could be thumbnail pictures, interface bars, content sections on a page, etc.
Setting the clone:false will make sure the unique objects are used (rather than clones)
Horizontal and vertical spacings can be set
Setting colSize and rowSize will override spacings and force grid dimensions
align and valign parameters can be used
setting a width or height will spread the items - and override the colSize and rowSize
In both dimensioned and non dimensioned modes,
the rows and columns can be squeezed together and aligned as a group
So a variety of layouts can be accommodated

See: https://zimjs.com/tile.html for a dynamic example

NOTE: ZIM Layout() is available to layout content that is scaled into regions
Tile will NOT scale the content but of course, the Tile itself can be scaled.
And items can be manually scaled before or after adding - but Tile does not do any scaling.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle();
var tile = new Tile(circle, 20, 10).center();
END EXAMPLE

EXAMPLE
// tile 10 rectangles spaced out across the stage
var tile = new Tile({obj:new Recangle(), cols:10, spacingH:30, width:stageW*.9).center();
END EXAMPLE

EXAMPLE
// tile two rows with a Dial and a Slider in each
var tile = new Tile({
	obj:series([new Dial(), new Slider(), new Dial(), new Slider]),
	cols:2, rows:2,
	spacingH:30, spacingV:30,
	valign:"center"
}).center();
// use tile.getChildAt(0) to access first Dial - or:
tile.items[0].on("change", function () {zog("changing dial 1")});

// OR if previously specified with events - use clone:false
var d1 = new Dial();
var d2 = new Dial();
var s1 = new Slider();
var s2 = new Slider();
var tile = new Tile({
	obj:series(d1, s1, d2, s2),
	cols:2, rows:2,
	spacingH:30, spacingV:30,
	valign:"center",
	clone:false // will use the original objects
}).center();
d1.on("change", function () {zog("changing dial 1")});
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
obj - |ZIM VEE| the display object to tile
	note: put rotated objects in a container (unless rotated 180 degrees) and tile the container
cols - (default 1) the columns to tile
rows - (default 1) the rows to tile
spacingH - (default 0) a spacing between columns - ignored if colSize is set
spacingV - (default 0) a spacing between rows - ignored if rowSize is set
width - (default null) set to a width to spread items in rows evenly out to width (see resize() method)
	colSize and mirrorH are ignored if width is set
height - (default null) set to a height to spread items in columns evenly out to height (see resize() method)
	rowSize and mirrorV are ignored if height is set
squeezeH - (default false) how to handle positioning within rows
	when width is not set:
		false - will not compress tiles beyond max calculated width for column including spacingH
		true - will treat all rows independently and fill spaces - align will align rows to widest row
	when width is set:
		false - will not compress past longest row including spacingH
		true - will compress all rows - align will then align rows to widest row
		"full" - will continue to compress always keeping each row to the width (may cause overlap)
squeezeV - (default "none") how to handle positioning within columns
	when height is not set:
		false - will not compress tiles beyond max calculated height for column including spacingV
		true - will treat all columns independently and fill spaces - valign will align columns to highest row
	when height is set:
		false - will not compress past longest column including spacingV
		true - will compress all columns - valign will then align columns to longest column
		"full" - will continue to compress always keeping each column to the height
colSize - |ZIM VEE| (default item size) set to number to hard code column width (ignores spacing)
	ignored if width is set
	use a series([100, 200, 100, 400]) to set specific sizes
rowSize - |ZIM VEE| (default item size) set to number to hard code row height (ignores spacing)
	ignored if height is set
	use a series([100, 200, 100, 400]) to set specific sizes
align - |ZIM VEE (non-squeezed only)| (default "left") set to "center", "middle", "right"
	this is a basic align and may not work with rotated objects
	add these to a container perhaps for best results
valign - |ZIM VEE (non-squeezed only)| (default "top") set to "center", "middle", "bottom"
count - (default cols*rows) optional total number of items to tile
	if count is set to 0 then count is ignored and a warning message is provided in console
mirrorH - (default false) flip alternating objects horizontally (works with top left registration point)
mirrorV - (default false) flip alternating objects vertically (works with top left registration point)
snapToPixel - (default true) sets the stage to snapToPixelEnabled and snaps to pixels to avoid small gaps when Tile is repositioned
clone - (default true) set to false to prevent Tile from cloning objects - good for series
style - (default true) set to false to ignore styles set with the STYLE - will receive original parameter defaults
group - (default null) set to String (or comma delimited String) so STYLE can set default styles to the group(s) (like a CSS class)
inherit - (default null) used internally but can receive an {} of styles directly

METHODS
remake(items) - pass in an array of items to tile - see items property for editing current list - returns tile for chaining
	can also change rows and cols and remake()
resize(width, height) - resize the tile with new width and/or height if the width and/or height parameters were set - returns tile for chaining
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
dispose() - removes listeners and deletes object

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
items - an array of the tile items - or use tile.getChildAt() or tile.loop(function (item) {})
tileNum - this property is added to each object in the tile to give its number in the tile
These properties can be changed by calling remake()
	items - an array of items the tile uses
		modify this and pass in to remake(items) to update the Tile with new items
		cannot modify count - count will become the length of the array passed to remake()
	cols - number of columns - can modify - need to call remake() to see changes
	rows - number of rows - can modify - need to call remake() to see changes
These properties can be changed by calling resize(width, height) - set width or height to 0 for no spreading
	spacingH - horizontal spacing - can modify - need to call resize() to see changes
	spacingV - vertical spacing - can modify - need to call resize() to see changes
	squeezeH - horizontal compression - can modify - need to call resize() to see changes
	squeezeV - vertical compression - can modify - need to call resize() to see changes
	align - (not with ZIM VEE) horizontal alignment - can modify - need to call resize() to see changes
	valign - (not with ZIM VEE) vertical alignment - can modify - need to call resize() to see changes
	mirrorH - horizontal mirroring - can modify - need to call resize() to see changes
	mirrorV - vertical mirroring - can modify - need to call resize() to see changes
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
	use resize(width, height) for resetting the width of the Tile without rescaling
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
	use resize(width, height) for resetting the height of the Tile without rescaling
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
group - used when the object is made to add STYLE with the group selector (like a CSS class)

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

--*///+66.5
	zim.Tile = function(obj, cols, rows, spacingH, spacingV, width, height, squeezeH, squeezeV, colSize, rowSize, align, valign, count, mirrorH, mirrorV, snapToPixel, clone, group, style, inherit) {
		var sig = "obj, cols, rows, spacingH, spacingV, width, height, squeezeH, squeezeV, colSize, rowSize, align, valign, count, mirrorH, mirrorV, snapToPixel, clone, group, style, inherit";
		var duo; if (duo = zob(zim.Tile, arguments, sig, this)) return duo;
		z_d("66.5");
		this.zimContainer_constructor(null,null,null,null,false);
		this.type = "Tile";

		this.group = group;
	    var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);
		if (zot(obj)) obj = DS.obj!=null?DS.obj:new zim.Circle();

		if (zot(cols)) cols = DS.cols!=null?DS.cols:1;
		if (zot(rows)) rows = DS.rows!=null?DS.rows:1;
		if (zot(width)) width = DS.width!=null?DS.width:null;
		if (zot(height)) height = DS.height!=null?DS.height:null;
		if (zot(colSize)) colSize = DS.colSize!=null?DS.colSize:null;
		if (zot(rowSize)) rowSize = DS.rowSize!=null?DS.rowSize:null;
		if (!zot(width)) colSize = null; // width and height override sizes
		if (!zot(height)) rowSize = null;

		if (zot(spacingH)) spacingH = DS.spacingH!=null?DS.spacingH:null;
		if (zot(spacingV)) spacingV = DS.spacingV!=null?DS.spacingV:null;
		if (zot(spacingH) || !zot(colSize)) spacingH = 0; // sizes override spacing
		if (zot(spacingV) || !zot(rowSize)) spacingV = 0;
		if (zot(squeezeH)) squeezeH = DS.squeezeH!=null?DS.squeezeH:false;
		if (zot(squeezeV)) squeezeV = DS.squeezeV!=null?DS.squeezeV:false;
		if (zot(align)) align = DS.align!=null?DS.align:"left";
		if (zot(valign)) valign = DS.valign!=null?DS.valign:"top";
		if (zot(count)) count = DS.count!=null?DS.count:null;
		if (count == 0) {count = null; if (zon) {zog("ZIM Tile() - count parameter of 0 is ignored - see docs");}}
		if (zot(mirrorH)) mirrorH = DS.mirrorH!=null?DS.mirrorH:false;
		if (zot(mirrorV)) mirrorV = DS.mirrorV!=null?DS.mirrorV:false;
		if (zot(snapToPixel)) snapToPixel = DS.snapToPixel!=null?DS.snapToPixel:true;
		if (snapToPixel) {
			if (obj && obj.stage) obj.stage.snapToPixelEnabled = true;
			else if (typeof(elem) !== 'undefined' && zimDefaultFrame) zimDefaultFrame.stage.snapToPixelEnabled = true;
		}
		if (zot(clone)) clone = DS.clone!=null?DS.clone:true;

		var that = this;
		that.cols = cols;
		that.rows = rows;
		that.spacingH = spacingH;
		that.spacingV = spacingV;
		that.squeezeH = squeezeH;
		that.squeezeV = squeezeV;
		var VEEAlign = (typeof align=="function");
		var VEEVAlign = (typeof valign=="function");
		// these two are used only for not ZIM VEE - as there are VEE with Squeeze is not supported
		that.align = VEEAlign?"left":align;
		that.valign = VEEVAlign?"top":valign;
		that.mirrorH = mirrorH;
		that.mirrorV = mirrorV;

		// ~~~~~~~~~~~~~~~~~~~  GET ARRAY OF ITEMS  ~~~~~~~~~~~~~~~
		// this list could be edited later and passed back into remake()
		// so need to separate this part from the rest
		// also, need to record what was made with zik so resize does not change items
		that.items = [];
		var tile;
		var currentCount = 0;
		outer:
		for (var j=0; j<rows; j++) {
			for (var i=0; i<cols; i++) {
				currentCount++;
				if (!zot(count) && currentCount > count) break outer;
				tile = zim.Pick.choose(obj);
				if (zot(tile)) {
					tile = new zim.Container(0,0,0,0);
				}
				// if (clone && !(j==0 && i==0)) tile = tile.clone();
				if (clone) {
					if ((j!=0 || i!=0)) tile = tile.clone();
				}
				that.items.push(tile);
				tile.tileNum = currentCount-1;
			}
		}

		// ~~~~~~~~~~~~~~~~~~~  ORIGINAL CALCULATIONS ~~~~~~~~~~~~~~~
		// collect col and row tile total sizes
		// with resize, we need to do this separately so zik is only run once

		var objects; // nested array of objects - recreated from linear items array
		var widthTotals; // for each row
		var heightTotals; // for each column
		var widthMax; // for each column
		var heightMax; // for each row
		var widthTotalMax; // for all columns
		var heightTotalMax; // for all rows
		var widthUncompressedMax; // for all columns uncompressed
		var heightUncompressedMax; // for all rows uncompressed
		var scalesX; // used for resizing mirror
		var scalesY;
		var rowCounts; // number of rows across columns
		var finalAlign; // so any zik col align is rememebered

		// remember any set widths and heights in case
		// zik is used for colSize and rowSize
		var widthHeights;

		function makeTile() {

			that.removeAllChildren();

			objects = []; // row arrays of picked objects in column
			widthTotals = []; // for each row
			heightTotals = []; // for each column
			widthMax = []; // for each column
			heightMax = []; // for each row
			widthTotalMax = 0; // for all columns
			heightTotalMax = 0; // for all rows
			widthUncompressedMax = 0; // for all columns uncompressed
			heightUncompressedMax = 0; // for all rows uncompressed
			scalesX = []; // used for resizing mirror
			scalesY = [];
			rowCounts = []; // number of rows across columns
			widthHeights = []; // width and heights per row per col [w,h]
			finalAlign = []; // remember horizontal aligns - vertical aligns are made in final loop

			if (!that.items || !that.items.length) that.items = [];
			count = that.items.length;
			var currentCount = 0;
			var tile;
			var j=0; var k=0;
			for (var i=0; i<count; i++) {
				j=Math.floor(i/that.cols);
				k=i%that.cols;
				if (zot(objects[j])) objects[j] = [];
				if (zot(that.items[i])) that.items[i] = new Container(0,0,0,0);
				objects[j][k] = that.items[i];
			}
			that.rows = Math.max(1, objects.length);
			that.cols = Math.max(1, !zot(objects[0])?objects[0].length:0);

			outer2:
			for (var j=0; j<that.rows; j++) {
				scalesX.push([]);
				scalesY.push([]);
				widthHeights.push([]);
				if (rowSize&&zot(height)) h = zim.Pick.choose(rowSize);
				for (var i=0; i<that.cols; i++) {
					currentCount++;
					if (!zot(count) && currentCount > count) break outer2;
					tile = objects[j][i];
					tile.snapToPixel = snapToPixel;
					that.addChild(tile);

					scalesX[j].push(tile.scaleX);
					scalesY[j].push(tile.scaleY);
					if (j==0 && (colSize&&zot(width))) w = zim.Pick.choose(colSize);
					if (j==0) finalAlign.push(zim.Pick.choose(align));
					if (!colSize||!zot(width)) w = Math.abs(tile.width);
					if (!rowSize||!zot(height)) h = Math.abs(tile.height);
					widthHeights[j][i] = [w,h];
					if (zot(widthMax[i])) widthMax[i] = 0;
					if (zot(heightMax[j])) heightMax[j] = 0;
					if (w > widthMax[i]) widthMax[i] = w;
					if (h > heightMax[j]) heightMax[j] = h;
					if (zot(widthTotals[j])) widthTotals[j] = 0;
					if (zot(heightTotals[i])) heightTotals[i] = 0;
					widthTotals[j] += w;
					heightTotals[i] += h;
					if (widthTotals[j] > widthTotalMax) widthTotalMax = widthTotals[j];
					if (heightTotals[i] > heightTotalMax) heightTotalMax = heightTotals[i];
					if (zot(rowCounts[i])) rowCounts[i] = 0;
					rowCounts[i]++;
				}
			}
			if (that.squeezeH === false) {
				zim.loop(widthMax, function(val) {
					widthUncompressedMax+=val;
				})
			}
			if (that.squeezeV === false) {
				zim.loop(heightMax, function(val) {
					heightUncompressedMax+=val;
				})
			}

		}
		makeTile();

		function resize(width, height) {

			// ~~~~~~~~~~~~~~~~~~~  GET WIDTHS ~~~~~~~~~~~~~~~

			if (width <= 0) width = null;
			if (height <= 0) height = null;

			var overallWidth;
			if (!zot(width)&&that.squeezeH=="full") {
				overallWidth = width;
			} else if (!zot(width)) {
				overallWidth = Math.max(widthTotalMax+(that.cols-1)*that.spacingH, width);
			} else {
				if (widthUncompressedMax > 0) overallWidth = widthUncompressedMax+(that.cols-1)*that.spacingH;
				else overallWidth = widthTotalMax+(that.cols-1)*that.spacingH;
			}

			var overallHeight;
			if (!zot(height)&&that.squeezeV=="full") {
				overallHeight = height;
			} else if (!zot(height)) {
				overallHeight = Math.max(heightTotalMax+(that.rows-1)*that.spacingV, height);
			} else {
				if (heightUncompressedMax > 0) overallHeight = heightUncompressedMax+(that.rows-1)*that.spacingV;
				else overallHeight = heightTotalMax+(that.rows-1)*that.spacingV;
			}

			that.setBounds(0,0,overallWidth,overallHeight);


			// ~~~~~~~~~~~~~~~~~~~  PLACEMENTS ~~~~~~~~~~~~~~~

			var rowObjects;
			var colTotal; // keep track of current x positions
			// loop order means we need to keep array data for y
			var rowTotals = []; // keep track of current y positions
			var rowSpacings = [];
			var rowTops = [];
			var rowTilesTotalWidth;
			for (j=0; j<objects.length; j++) {
				rowObjects = objects[j];
				colTotal = 0;
				if (!zot(width)) {rowTilesTotalWidth = 0;}
				for (i=0; i<rowObjects.length; i++) {
					tile = rowObjects[i];
					var b;
					var left;
					var top;
					var spreadXspacing;
					var spreadYspacing;
					if (j==0) {
						rowTotals[i] = 0;

						// ~~~~~~~~~~~~~~~~~~~  VERTICAL ALIGN ~~~~~~~~~~~~~~~

						if (!zot(height)) {
							if (that.squeezeV=="full") {
								rowSpacings[i] = rowCounts[i]>1?((height - heightTotals[i]) / (rowCounts[i]-1)):0;
							} else if (that.squeezeV) {
								rowSpacings[i] = rowCounts[i]>1?Math.max(that.spacingV, (height - heightTotals[i]) / (rowCounts[i]-1)):0;
							} else {
								rowSpacings[i] = rowCounts[i]>1?Math.max(that.spacingV, (overallHeight - heightTotals[i]) / (rowCounts[i]-1)):0;
							}
						}
						if (that.squeezeV) {
							// check for center or bottom valign of whole row
							if (that.valign=="center" || that.valign=="middle") {
								rowTops[i] = (overallHeight-(heightTotals[i]+(rowCounts[i]-1)*(!zot(height)?rowSpacings[i]:that.spacingV)))/2;
							} else if (that.valign=="bottom") {
								rowTops[i] = overallHeight-(heightTotals[i]+(rowCounts[i]-1)*(!zot(height)?rowSpacings[i]:that.spacingV));
							} else {
								rowTops[i] = 0;
							}
						} else {
							rowTops[i] = 0;
						}
					}

					b = tile.getBounds();
					if (i==0) {

						// ~~~~~~~~~~~~~~~~~~~  HORIZONTAL ALIGN ~~~~~~~~~~~~~~~

						if (!zot(width)) {
							if (that.squeezeH=="full") {
								spreadXspacing = rowObjects.length>1?((width - widthTotals[j]) / (rowObjects.length-1)):0;
							} else if (that.squeezeH) {
								spreadXspacing = rowObjects.length>1?Math.max(that.spacingH, (width - widthTotals[j]) / (rowObjects.length-1)):0;
							} else {
								spreadXspacing = rowObjects.length>1?Math.max(that.spacingH, (overallWidth - widthTotals[j]) / (rowObjects.length-1)):0;
							}
						}
						if (that.squeezeH) {
							// check for center or right align of whole row
							if (that.align=="center" || that.align=="middle") {
								left = (overallWidth-(widthTotals[j]+(rowObjects.length-1)*(!zot(width)?spreadXspacing:that.spacingH)))/2;
							} else if (that.align=="right") {
								left = overallWidth-(widthTotals[j]+(rowObjects.length-1)*(!zot(width)?spreadXspacing:that.spacingH));
							} else {
								left = 0;
							}
						} else {
							left = 0;
						}

						// squeeze does not work with ZIK - so this is for non-squeezed
						finalVAlign = zim.Pick.choose(valign); // start of each row pick a valign
					}
					// squeeze does not work with ZIK - here is the recorded align for the current column
					align = finalAlign[i];

					var w = widthHeights[j][i][0];
					var h = widthHeights[j][i][1];

					if (that.mirrorH && i%2==1) {
						tile.scaleX = -scalesX[j][i];
						// tile.x = colTotal+w-b.x*2*tile.scaleX;
					} else {
						// tile.x = colTotal + (tile.regX-b.x);
					}
					tile.pos(colTotal, null);

					if (!that.squeezeH && VEEAlign) {
						if (zot(width) && (align=="center" || align=="middle")) {
							tile.x += (widthMax[i]-tile.width)/2;
						} else if (zot(width) && align=="right") {
							tile.x += widthMax[i]-tile.width;
						}
					} else if (!that.squeezeH) { // this allows for dynamic setting of align (for non-VEE, non squeezeH)
						if (zot(width) && (that.align=="center" || that.align=="middle")) {
							tile.x += (widthMax[i]-tile.width)/2;
						} else if (zot(width) && that.align=="right") {
							tile.x += widthMax[i]-tile.width;
						}
					} else {
						if (zot(width) && (that.align=="center" || that.align=="middle")) {
							tile.x += (w-tile.width)/2;
						} else if (zot(width) && that.align=="right") {
							tile.x += w-tile.width;
						}
					}
					if (that.mirrorV && j%2==1) {
						tile.scaleY = -scalesY[j][i];
						// tile.y = rowTotals[i]+h-b.y*2;
					} else {
						// tile.y = rowTotals[i] + tile.regY-b.y;
					}
					tile.pos(null,rowTotals[i]);
					if (!that.squeezeV && VEEVAlign) {
						if (zot(height) && (finalVAlign=="center" || finalVAlign=="middle")) {
							tile.y += (heightMax[j]-tile.height)/2;
						} else if (zot(height) && finalVAlign=="bottom") {
							tile.y += heightMax[j]-tile.height;
						}
					} else if (!that.squeezeV) { // this allows for dynamic setting of valign (for non-VEE, non squeezeV)
						if (zot(height) && (that.valign=="center" || that.valign=="middle")) {
							tile.y += (heightMax[j]-tile.height)/2;
						} else if (zot(height) && that.valign=="bottom") {
							tile.y += heightMax[j]-tile.height;
						}
					} else {
						if (zot(height) && (that.valign=="center" || that.valign=="middle")) {
							tile.y += (h-tile.height)/2;
						} else if (zot(height) && that.valign=="bottom") {
							tile.y += h-tile.height;
						}
					}

					if (that.squeezeH === true || !zot(width)) {
						colTotal += w+(!zot(width)?spreadXspacing:that.spacingH);
					} else {
						colTotal += widthMax[i]+(!zot(width)?spreadXspacing:that.spacingH);
					}


					tile.x += left;
					tile.y += rowTops[i];

					if (that.squeezeV === true || !zot(height)) {
						rowTotals[i] += h+(!zot(height)?rowSpacings[i]:that.spacingV);
					} else {
						rowTotals[i] += heightMax[j]+(!zot(height)?rowSpacings[i]:that.spacingV);
					}

				}

			}
		} // end resize

		resize(width, height);

		this.remake = function(items) {
			if (!zot(items)) that.items = items;
			makeTile();
			resize(width, height);
			return that;
		}

		this.resize = function(w, h) {
			if (zot(w)) w = width;
			if (zot(h)) h = height;
			width = w;
			height = h;
			resize(w, h);
			return that;
		}

		if (style!==false) zimStyleTransforms(this, DS); // global function - would have put on DisplayObject if had access to it

		this.clone = function() {
			return that.cloneProps(new zim.Tile(obj.clone?obj.clone():obj, that.cols, that.rows, that.spacingH, that.spacingV, width, height, that.squeezeH, that.squeezeV, colSize, rowSize, align, valign, that.items.length, that.mirrorH, that.mirrorV, snapToPixel, this.style, this.group));
		}
	}
	zim.extend(zim.Tile, zim.Container, "clone", "zimContainer", false);
	//-66.5

/*--
zim.Layout = function(holder, regions, lastMargin, backgroundColor, vertical, regionShape, scalingObject, hideKey, style, group, inherit)

Layout
zim class - extends a createjs.EventDispatcher

DESCRIPTION
Layout arranges objects on the page by fitting (scaling) them in regions.
Make a layout object for each page if desired
and even nest layout objects inside regions.
Fixed aspect ratio content is fit into regions.
Layout is good for flexive design where you anchor titles and navigation.
Layout handles any number of regions vertically or horizontally.
It is useful for full scale mode for different devices or browser window scale.
You need to run the resize() method to update the layout.
Put the all your layouts in LayoutManager to scale all at once.

NOTE: ZIM Tile is available to lay out non-scaled content

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// these would be containers with your content
// make sure that bounds are set on containers
// you may want to hard code bounds for clarity
var header = new Rectangle(500, 200, "blue");
var content = new Rectangle(600, 500, "green");
var footer = new Rectangle(500, 200, "blue");
stage.addChild(header, content, footer);

// make the Layout - more useful for FULL scale mode
var layout = new Layout({
	holder:stage,
	regions:[
		{object:header, marginTop:10, maxWidth:80, minHeight:10, valign:"top"},
		{object:content, marginTop:5, maxWidth:90}, // note, middle gets no minHeight
		{object:footer, marginTop:5, maxWidth:80, height:10}
	],
	lastMargin:5
});

// add to LayoutManager to resize or dispose all layouts together
// disposing only removes keyboard events to show boundaries
var manager = new LayoutManager();
manager.add(layout);

frame.on("resize", function() {
	manager.resize();
	stage.update();
});

stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
holder - object to hold layout (stage, container, etc) that must have bounds set
regions - an array of region objects with specific properties for each
	Example VERTICAL region objects - all dimensions are percents
		[{object:title, marginTop:10, maxWidth:80, minHeight:20, align:"left", valign:"top"},
		{object:content, marginTop:5, maxWidth:90}, // note, middle gets no minHeight
		{object:nav, marginTop:5, maxWidth:80, height:20, backgroundColor:"red"}]
	note: no minHeight for middle regions - but heights on any region
	align defaults to middle for the regions
	valign defaults to top and bottom for the top and bottom region and middle for the others
	backgroundColor applies a backing color to the region
	Example HORIZONTAL region objects
		[{object:col1, marginLeft:10, maxHeight:80, width:20, valign:"bottom"},
		{object:col2, marginLeft:5, maxHeight:90, align:"middle"}, // note, middle gets no minWidth
		{object:col3, marginLeft:5, maxHeight:80, minWidth:20, align:"left", valign:"top"}]
	align defaults to left and right for the outer regions and middle for the inside regions
	valign defaults to top for all the regions
lastMargin - (default 0) the margin at the bottom (vertical) or at the right (horizontal)
backgroundColor - (default null) background color for the whole holder
vertical - (default true) set to false for horizontal layout
regionShape - (default null) a zim or createjs Shape object to show bounds (gets added to holder)
	can toggle on and off with B key - but must pass in the Shape to use the B key
scalingObject - (default holder) an object used as the bounds of the region scaling
	setting a scalingObject will also set the bounds of the holder to the scalingObject bounds
	it does not scale the holder - only scales the region objects inside
hideKey - (default B) is the hot key for hiding and showing the bounds

METHODS
resize() - resize based on new bounds of the holder (or scalingObject)
dispose() - removes the B key listener (otherwise, nothing to dispose)
toggle(state - default null) - toggle() will show controls if they are hidden or hide controls if they are showing
	alternatively, pass in true to show controls or false to hide controls
	note - method does not update the stage
addShape(shape) - adds a bounding shape dynamically
removeShape() - permanently removes the bounding shape
disable() - disables all the layout (shape and sizing)
enable() - enables all the layout (shape and sizing)
if you want to get rid of the objects then you need to do so in your app

PROPERTIES
type - holds the class name as a String
regions - the regions object - if changed will have to call resize() manually
toggled - read-only Boolean as to whether the controls are showing or hidden

DESCRIPTION OF FLEXIVE DESIGN
here described with vertical layout - horizontal is similar but rotated 90
the content in the middle will try and expand against the top and bottom
until it forces the top and bottom to their minimum percents
if the content hits its maximum width percent first then the top and bottom
will fill up the rest of the height until they reach their maximum widths
--*///+80
	zim.Layout = function(holder, regions, lastMargin, backgroundColor, vertical, regionShape, scalingObject, hideKey, style, group, inherit) {
		var sig = "holder, regions, lastMargin, backgroundColor, vertical, regionShape, scalingObject, hideKey, style, group, inherit";
		var duo; if (duo = zob(zim.Layout, arguments, sig, this)) return duo;
		z_d("80");
		this.cjsEventDispatcher_constructor();
		this.type = "Layout";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(holder)) holder = DS.holder!=null?DS.holder:null;
		if (zot(scalingObject)) scalingObject = DS.scalingObject!=null?DS.scalingObject:null;

		if (zot(holder) || !holder.getBounds) {zog ("zim controls - Layout(): please provide an object with bounds set that holds the objects being laid out"); return;}
		scalingObject = (zot(scalingObject)) ? holder : scalingObject;
		if (!scalingObject.getBounds || !scalingObject.getBounds()) {zog ("zim controls - Layout(): holder must have bounds set or provide a scalingObject with bounds"); return;}
		var bounds = scalingObject.getBounds();
		holder.setBounds(0,0,bounds.width,bounds.height);
		// note, Layout sets bounds of holder but does not scale the holder - only the objects in regions
		// it may be that the holder is scaled by some external process
		// but probably not if a scalingObject is used

		if (zot(lastMargin)) lastMargin = DS.lastMargin!=null?DS.lastMargin:0;
		if (zot(vertical)) vertical = DS.vertical!=null?DS.vertical:true;
		if (zot(backgroundColor)) backgroundColor = DS.backgroundColor!=null?DS.backgroundColor:"";
		if (zot(hideKey)) hideKey = DS.hideKey!=null?DS.hideKey:"B";
		if (zot(regionShape)) regionShape = DS.regionShape!=null?DS.regionShape.clone():null;
		var backing = new createjs.Shape(); // holds any backing colors
		var that = this;
		this.active = true;

		// loop through region objects and assign defaults
		// also check that regions can fit with values given
		// we basically do the same thing with horizontal and vertical layouts
		// but obviously one uses widths and the other heights, etc.
		// so adapted generic phrases of PRIMARY and SECONDARY
		// primary for vertical is in the Y direction and uses height and top
		// primary for horizontal is in the X direction and uses width and left
		// secondary for vertical is X and for horizontal is Y
		// min, absolute and margin values are only available in the primary
		// max values are only available in the secondary
		// align, valign and backgroundcolor is available for primary and secondary
		// absolute values (height, width) are to be used if given
		// if not given we try to maximize size and to adhere to min values
		// as calculations progress we calculate given, maxGiven and marginGiven values
		// these are temporary depending on the resizing and are always in the primary direction
		// secondary direction is quite simple
		// primary direction is quite complex involving a number of steps and even some recursion

		if (zot(regions)) regions = DS.regions!=null?zim.copy(DS.regions):null;
		if (zot(regions)) return;
		this.regions = regions; // expose the regions object for dynamic adjustments then manual resize

		var r; // used to hold a region in a loop
		var totalAbsolute = 0;
		var minPrimary = "minWidth";
		var primary = "width";
		var secondary = "height";
		var marginPrimary = "marginLeft";
		var maxSecondary = "maxHeight";
		var axisPrimary = "x";
		var axisSecondary = "y";
		if (vertical) {
			minPrimary = "minHeight";
			primary = "height";
			secondary = "width";
			marginPrimary = "marginTop";
			maxSecondary = "maxWidth";
			axisPrimary = "y";
			axisSecondary = "x";
		}
		for (var i=0; i<regions.length; i++) {
			r = regions[i];
			if (!r.object || !r.object.getBounds()) {zog("zim controls - Layout(): each region object must have an object with setBounds() set"); return;}
			if (!r[minPrimary]) r[minPrimary] = 0;
			if (!r[primary]) r[primary] = 0;
			if (!r.backgroundColor) r.backgroundColor = "";
			r.given = 0;
			r.maxGiven = 0;
			if (zot(r[marginPrimary])) r[marginPrimary] = .0001;
			if (!r[maxSecondary]) r[maxSecondary] = 100;
			if (vertical) {
				// default alignment differs for orientation
				if (!r.align) r.align = "middle";
				if (!r.valign) {
					if (i==0) {r.valign = "top";}
					else if (i==regions.length-1) {r.valign = "bottom";}
					else {r.valign = "middle";}
					if (regions.length == 1) {r.valign = "middle"}
				}
			} else {
				if (!r.valign) r.valign = "top";
				if (!r.align) {
					if (i==0) {r.align = "left";}
					else if (i==regions.length-1) {r.align = "right";}
					else {r.align = "middle";}
					if (regions.length == 1) {r.align = "middle"}
				}
			}
			if (r[primary]) r[minPrimary] = 0; // primary overrides minPrimary
			totalAbsolute += r[primary] + r[marginPrimary];
		}

		// primaries (not minPrimaries) are absolute percentage and are kept no matter what
		// margins are absolute percentage and are kept no matter what
		// check if primaries and margins are more than 100%
		totalAbsolute += lastMargin;
		if (totalAbsolute > 100) {zog("zim controls - Layout(): cannot fit regions into 100% bounds"); return;}
		var leftOverPrimary = 100-totalAbsolute;

		distribute(); // also called from within resize function
		function distribute() {
			// distribute leftOverPrimary to any regions without a primary or a given (primary)
			// proportion based on primary dimension of objects in regions
			// apply this primary to given (primary)
			var totalPrimaries = 0;
			for (var i=0; i<regions.length; i++) {
				r = regions[i];
				r.given = 0;
				if (r[primary] == 0) totalPrimaries += r.object.getBounds()[primary];
			}
			// now we know total raw heights of objects needing height applied
			// loop back through and give these objects their proportion of what is left
			for (var i=0; i<regions.length; i++) {
				r = regions[i];
				if (r[primary] == 0) r.given = r.object.getBounds()[primary] / totalPrimaries * leftOverPrimary;
			}
		}

		this.resize = function() {
			if (!that.active) return;
			bounds = scalingObject.getBounds();
			holder.setBounds(0,0,bounds.width,bounds.height);
			backing.graphics.clear();
			if (backgroundColor!="") backing.graphics.f(backgroundColor).r(0,0,bounds.width,bounds.height);

			for (var i=0; i<regions.length; i++) {
				r = regions[i];
				r.maxGiven = 0;
				r.marginGiven = 0;
			}
			// all the primaries are applied
			// but some objects might not need the primary because they have maxed out on maxSecondary
			// we need to give this extra primary back to the pool
			// and keep doing it until there are no more maxed objects

			var keepGoing = true; var allCheck; var giveBack;
			var p; var s; var boundsP; var boundsS; var maxGiven;
			var leftOverPrimary2 = leftOverPrimary;
			while (keepGoing) {
				// check for objects maxed in width
				giveBack = 0;
				keepGoing = false; allCheck = true;
				// we want to keep going unless all objects are maxed
				// or none of the objects are maxed
				for (var i=0; i<regions.length; i++) {
					r = regions[i];
					if (r.given > 0 && r.maxGiven == 0) {
						p = r.object.getBounds()[primary];
						s = r.object.getBounds()[secondary];
						boundsP =  r.given * bounds[primary]/100;
						boundsS =  r[maxSecondary] * bounds[secondary]/100; // convert to pixels
						maxGiven = s/p*boundsP;
						if (maxGiven > boundsS) {
							// maxed out so give back height
							// keepGoing=true;
							// store this as maxGiven property
							// might have to take it away if later minHeights are not met
							r.maxGiven = p/s*boundsS * 100/bounds[primary]; // convert back to percentage
							giveBack += r.given - r.maxGiven;
							leftOverPrimary2 -= r.maxGiven;
						} else {
							allCheck = false;
						}
					}
				}

				// !keepGoing was missing when secondary affects primary so took check out
				// if (!keepGoing) break;
				if (allCheck) break;

				// redistribute the extra stuff too all that are not maxed out and not with primary values
				// proportion based on primary dimension of objects in regions
				// apply this primary to given (primary)
				totalPrimaries = 0;
				for (var i=0; i<regions.length; i++) {
					r = regions[i];
					if (r[primary] == 0 && r.maxGiven == 0) totalPrimaries += r.object.getBounds()[primary];
				}
				// now we know total raw heights of objects needing height applied
				// loop back through and give these objects their proportion of what is left
				for (var i=0; i<regions.length; i++) {
					r = regions[i];
					if (r[primary] == 0 && r.maxGiven == 0) r.given = r.object.getBounds()[primary] / totalPrimaries * leftOverPrimary2;
				}
			}

			// if end regions have not met their minPrimaries
			// set those minPrimaries to primaries and resize again
			// divide leftover primary to regions with no set primary
			// maximize middle regions as this is usually content
			// if the edge regions have minPrimaries set them to minPrimary
			// if they do not have minPrimaries then proportion them equally with the rest

			var scaleCheck = true;
			r = regions[0];
			if (r.maxGiven > 0) {
				 if (r.maxGiven < r[minPrimary]) {r[primary] = r[minPrimary]; scaleCheck = false;}
			} else if (r.given > 0) {
				 if (r.given < r[minPrimary]) {r[primary] = r[minPrimary]; scaleCheck = false;}
			}
			r = regions[regions.length-1];
			if (r.maxGiven > 0) {
				 if (r.maxGiven < r[minPrimary]) {r[primary] = r[minPrimary]; scaleCheck = false;}
			} else if (r.given > 0) {
				 if (r.given < r[minPrimary]) {r[primary] = r[minPrimary]; scaleCheck = false;}
			}
			if (!scaleCheck) {
				// recalculate leftOverPrimary
				totalAbsolute = 0;
				for (var i=0; i<regions.length; i++) {
					r = regions[i];
					totalAbsolute += r[primary] + r[marginPrimary];
				}
				totalAbsolute += lastMargin;
				if (totalAbsolute > 100) {zog("zim display - Layout(): cannot fit regions into 100% bounds"); return;}

				leftOverPrimary = 100-totalAbsolute;
				distribute();
				that.resize();
				return;
			}

			// if specified all primaries or all maxed in secondary
			// then distribute based on inner margins
			// watch out - may need to revert to original margins if page is resized
			// so introduce a new marginGiven property

			var allHeights = true; var marginTotal = 0; var primaryTotal = 0;
			for (var i=0; i<regions.length; i++) {
				r = regions[i];
				marginTotal += r[marginPrimary];
				if (r[primary] > 0) primaryTotal += r[primary];
				else if (r.maxGiven > 0) primaryTotal += r.maxGiven;
				else if (r.given > 0) primaryTotal += r.given;
				if (r[primary] == 0) {
					allHeights = false;
				}
			}
			if (allHeights || allCheck) {
				marginTotal += lastMargin;
				var extra = 100-primaryTotal-marginTotal;
				// remove two outer margins
				marginTotal -= (lastMargin + regions[0][marginPrimary]);
				if (extra != 0 && marginTotal != 0) { // divide up extra margin space
					for (var i=0; i<regions.length; i++) {
						if (i==0) continue;
						r = regions[i];
						r.marginGiven = r[marginPrimary]/marginTotal*(marginTotal+extra);
					}
				}
			}

			// ready to fit objects into regions, align and draw any bounds and background colors
			var pPos=0; // primary position (x for horizontal, y for vertical)
			var sPos=0; // secondary position
			var p;  	// primary dimension (width for horizontal, height for vertical)
			var s;		// secondary dimension
			var f; 		// fit variable will receive a handy object with new data and original region bounds data
						// {x:obj.x, y:obj.y, width:newW, height:newH, scale:scale, bX:left, bY:top, bWidth:width, bHeight:height}

			var addedW; var addedH;	// just a little offscreen coloring to help page transitions
			if (regionShape && regionShape.graphics) {
				var g = regionShape.graphics;
				g.c();
			}
			for (var i=0; i<regions.length; i++) {
				r = regions[i];

				// calculate primary data
				if (r.marginGiven > 0) pPos += r.marginGiven * bounds[primary]/100; // convert to pixels
				else pPos += r[marginPrimary] * bounds[primary]/100;
				if (r[primary] > 0) {p = r[primary];}
				else if (r.maxGiven > 0) {p = r.maxGiven;}
				else if (r.given > 0) {p = r.given;}
				else {p = 0;}
				p = p * bounds[primary]/100;

				// calculate secondary data
				s = r[maxSecondary] * bounds[secondary]/100;
				sPos = (bounds[secondary]-s)/2;

				// fit the objects into the region, align and draw any regionShape
				// this is slightly different for different orientations
				if (vertical) f = zim.fit(r.object,sPos,pPos,s,p);
				else f = zim.fit(r.object,pPos,sPos,p,s);

				// handle alignment
				if (r.valign == "top") r.object.y = f.bY;
				else if (r.valign == "bottom") r.object.y = f.bY+f.bHeight-f.height;
				if (r.align == "left") r.object.x = f.bX;
				else if (r.align == "right") r.object.x = f.bX+f.bWidth-f.width;
				if (regionShape && regionShape.graphics) {
					g.s("white").ss(2).r(f.bX,f.bY,f.bWidth,f.bHeight);
					g.s("#ff8203").sd([20,20]).r(f.bX,f.bY,f.bWidth,f.bHeight).sd();
				}

				// draw any backing colors for region
				// transitions in ZIM Pages need a little extra overlap on page edges
				addedH = addedW = 0;
				if (pPos == 0 || (pPos+p) == bounds[primary]) if (vertical) {addedH=1} else {addedW=1};
				if (s == bounds[secondary]) if (vertical) {addedW=1} else {addedH=1};
				if (r.backgroundColor != "") backing.graphics.f(r.backgroundColor).r(f.bX, f.bY, f.bWidth+addedW, f.bHeight+addedH);

				// increase our primary position
				pPos += p;
			}
		}

		this.resize();

		// add regionShape if there is one and backing shape
		if (regionShape) holder.addChild(regionShape);
		holder.addChildAt(backing,0);

		// key listener and other methods:

		// add key listener to hide and show the bounds
		window.addEventListener("keydown", keyEvent);
		function keyEvent(e) {
			if (!e) e=event;
			if (regionShape) {
				if (String.fromCharCode(e.keyCode) == hideKey.toUpperCase()) { // B
					that.toggle();
					if (regionShape.stage) regionShape.stage.update();
				}
			}
		}

		this.disable = function() {
			that.active = false;
			window.removeEventListener("keydown", keyEvent);
			if (regionShape) regionShape.alpha = 0;
		}

		this.enable = function() {
			that.active = true;
			window.addEventListener("keydown", keyEvent);
			that.resize();
			if (regionShape) regionShape.alpha = 1;
		}

		this.removeShape = function() { // use for final app
			if (regionShape) {
				regionShape.graphics.clear();
				holder.removeChild(regionShape);
				regionShape = null;
				regionShape = false;
			}
			window.removeEventListener("keydown", keyEvent);
		}

		this.toggled = true;
		this.toggle = function(type) {
			if (type === true) {
				regionShape.visible = true;
			} else if (type === false) {
				regionShape.visible = false;
			} else {
				regionShape.visible = !regionShape.visible;
			}
			that.toggled = regionShape.visible;
			return that;
		}

		this.addShape = function(shape, target) {
			that.removeShape();
			regionShape = shape;
			window.addEventListener("keydown", keyEvent);
			holder.addChild(regionShape);
			that.resize();
		}

		if (style!==false) zimStyleTransforms(this, DS);

		this.dispose = function() {
			// does not really dispose in case a resize is needed
			// it has no events aside from the keydown
			// which gets removed when we removeShape below
			that.removeShape();
			return true;
		}
	}
	zim.extend(zim.Layout, createjs.EventDispatcher, null, "cjsEventDispatcher", false);
	//-80

/*--
zim.Accessibility = function(appName, tabOrder, tabIndex, cycle, frame, decimals, alwaysHighlight, AHTime, AHColor, AHBorderWidth, AHBorderPadding, AHAlpha, AHObject, AHObjectScale)

Accessibility
zim class - extends a createjs.EventDispatcher

DESCRIPTION
Adds Screen Reader accessibility to the canvas for TAB key or Swipe (mobile) highlighting of ZIM objects
Some objects can be activated using the ENTER key and adjusted using the ARROW keys
Default or custom titles can be set to be read by the Screen Reader
The objects and the order in which the objects recieve focus can be set with a tabOrder array
A text message can be passed to the talk() method and it will be read by a Screen Reader

NOTE: Instructions to activate a screen reader on desktop or laptop computers
On Windows, you can type Narrator into Cortana and run it - it is really easy
On Mac, under Accessibility choose Voice Over
On Android, under Accesibility choose Voice and turn on TalkBack
Windows worked at ZIM 6, Apple worked at 6.1.0, Android worked at 6.1.0 aside from Slider, Dial, Stepper and ColorPicker
Custom readers were not tested

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var button = new Button().center(stage)
button.on("mousedown", function() {
	zgo("http://zimjs.com");
});
var dial = new Dial().center(stage).mov(200);

var accessibility = new Accessibility();
// this will automatically read in all objects on the stage and give default messages for the Screen Reader
// ENTER key events will be added to objects that will translate to mousedown and click events on the object
// Tab (swipe on mobile) to focus on the Button and press enter (double tap on mobile) to go to the ZIM site
// The Dial can use arrows to increase and decrease its value (up and right increase, down and left decrease)
// On mobile, double tapping the Dial brings up a select box with options (as does Slider, Stepper, and ColorPicker)

// OR

var accessibility = new Accessibility("Second Example");
accessibility.tabOrder = [
	dial,
	{obj:button, title:"Press ENTER to go to ZIM site"}
];
// this will start and end the app with "Second Example" being read (rather than default, "application")
// the dial will be the first item to tab to
// the button has a tabOrder object so it will have the title read rather than the default button message
// You could also provide a tabOrder object for the dial as well
END EXAMPLE

NOTE: Please see https://zimjs.com/screenreader for a detailed example
NOTE: Please see https://zimjs.com/accessibility for a more examples

PARAMETERS supports DUO - parameters or single object with properties below
appName - (default "application") read in screen reader when application receives or loses focus
tabOrder - (default an array of all ZIM Display Objects on stage) an array of zim Display Objects
 	These will be given TAB key control (and ENTER and ARROWS) and will work with Screen Readers
	Or given swipe left/right and double tap on mobile
	*** Alternatively, an array of tabOrder objects with an obj property and a title property can be used
	The obj is the Dispay Object and the title is what is read by the Screen Reader
	eg. {obj:button, title:"Press Enter Key to start game"}
	Can also specify tabOrder as a property of Accessibility
	*** The tabOrder may change compared to the array that is initially provided
	*** as RadioButtons, Picker, Tabs, and Pad components are split into separate items
	*** For objects not on the stage including a Pane, Waiter, etc. do not rely on the auto adding, use the parameter specified array instead.
tabIndex - (default -1) - a starting index for focus - or can set tabIndex property after object is made
cycle - (default false) set to true to keep tab order inside application rather than leaving application when an end is reached
decimals - (default 2) number of decimals max to read for screen reader
frame - (default currentFrame) the frame
alwaysHighlight - (default false) screen readers will add their own highlights - but this will set highlight to true even if there is no screen reader
	Set to true to place a rectangle around the object being put into focus by pressing the tab key or swipe on mobile
	This will replace screen reader highlights (eg. for Windows Narrator) except for when aria is true (eg. Apple Voice Over)
	The rest of the parameters relate to the alwaysHighlight - meaning highlight even if there is no screen reader
AHTime - (default 700ms) milliseconds to show the alwaysHighlight
AHColor - (default brown) - the color of the alwaysHighlight rectangle
AHBorderWidth - (default 3) thickness of border
AHBorderPadding - (default 5) distance from object bounds outward towards border
AHAlpha - (default .8) alpha of the alwaysHighlight
AHObject - (default null) set to a display object - including animated objects - to override the rectangle as a alwaysHighlight object
AHObjectScale - (default .8) scale the AHObject relative to the object with tab focus

METHODS
tab(dir) - set dir to 1 (default) to emulate tab forward or -1 to emulate shift tab
changeTitle(target, title, activate) - change a title for the Screen Reader
	target - the tabObject (eg. button) or the tabIndex of the item in the tabOrder to change
		*** The tabOrder may change compared to the array that is initially provided
		*** as RadioButtons, Picker, Tabs, and Pad components are split into separate items
	title - the new title that will be read to the screen reader
		If no title is provided any component passed will just update to its currentValue or selectedIndex
	activate (default false) - set to true to set focus of item at index and send to Screen Reader
talk(words) - tell the Screen Reader to read the words provided (does not affect focus)
resize(target) - target is the object or index of the object to update - or do not pass a target to update all
	This needs to be done if the object is moved, scaled, or removed from / re-added to the stage
	Accessibility works by placing HTML tags behind the canvas where the ZIM objects exist - so resize() handles this
	Use the Frame resize event and optionally, the ResizeManager()
dispose() - removes listeners and sets tabOrder to []

PROPERTIES
type - holds the class name as a String
tabOrder - get or set an array with the order in which display objects will receive focus with tab and shift tab (swipe on mobile)
tabIndex - get or set the index of the tabOrder (also see currentObject)
	Setting works only if object at the index is on the stage
	Returns -1 if no tabOrder object has the focus
currentObject - get or set the object in the tabOrder that has focus
	Objects have the following Accessibility properties added:
		zimAccessibility - the accessibility object
		zimTabIndex - the index in the tabOrder
		zimTabTag - the HTML tag that is used to represent the object to the screen reader
		zimTabParent - the parent of an object for RadioButtons, Tabs, and Pads (for others, the zimTabParent is the object)
		tabIndex - the index of the tag in tabParent (if there is a parent)
		type - the type of object. If there is a zimTabParent (that is not itself), the type is RadioButton, TabsButton or PadButton
activatedObject - get the object in the tabOrder that was last clicked or had the ENTER key pressed on
startAppTag - get the HTML tag that announces application start
endAppTag - get the HTML tag that announces application end
cycle - (default false) set to true to keep tab order inside application rather than leaving application when an end is reached
decimals - (default 2) number of decimals max to read for screen reader
frame - (default currentFrame) the frame
alwaysHighlight - Boolean to use a alwaysHighlight rectangle
AHTime - milliseconds to show the hightlight
AHColor - the color of the alwaysHighlight rectangle
AHBorderWidth - thickness of border
AHBorderPadding - (default 5)distance from object bounds outward towards border
AHAlpha - alpha of the alwaysHighlight
AHObject - set to a display object - including animated objects - to override the rectangle as a alwaysHighlight object
AHObjectScale - scale the AHObject relative to the object with tab focus
enabled - default is true - set to false to disable

EVENTS
Dispatches a "change" event when the screen reader is about to talk
	This is when the talk() method runs or the tabIndex is changed (from click, swipe, tab, changeTitle - with activate true)
	The event object has a title property that holds the words the screen reader will say
 	Several change events can happen at the same time so what is said is usually the last
	but the talk() method takes priority as it runs in alert mode so focus is not lost
 The Enter key dispatches mousedown and click events from object with focus
	The event object has a fromEnter property which is true if from an enter key on the object
	This could trigger a button press for instance

--*///+30.5
	zim.Accessibility = function(appName, tabOrder, tabIndex, cycle, decimals, frame, alwaysHighlight, AHTime, AHColor, AHBorderWidth, AHBorderPadding, AHAlpha, AHObject, AHObjectScale) {
		var sig = "appName, tabOrder, tabIndex, cycle, decimals, frame, alwaysHighlight, AHTime, AHColor, AHBorderWidth, AHBorderPadding, AHAlpha, AHObject, AHObjectScale";
		var duo; if (duo = zob(zim.Accessibility, arguments, sig, this)) return duo;
		z_d("30.5");
		this.cjsEventDispatcher_constructor();
		this.type = "Accessibility";

		if (zot(appName)) appName = "application";
		if (zot(cycle)) cycle = false;
		if (zot(decimals)) decimals = 2;
		if (zot(frame)) frame = zimDefaultFrame;
		if (zot(alwaysHighlight)) alwaysHighlight = false;
		if (zot(AHTime)) AHTime = 700;
		if (zot(AHColor)) AHColor = "brown";
		if (zot(AHBorderWidth)) AHBorderWidth = 3;
		if (zot(AHBorderPadding)) AHBorderPadding = 5;
		if (zot(AHAlpha)) AHAlpha = .8;
		if (zot(AHObjectScale)) AHObjectScale = .8;
		if (zot(AHObject)) {
			var alwaysHighlightShape = new zim.Shape({style:false});
			alwaysHighlightShape.mouseEnabled = false;
		} else {
			if (AHObject.mouseEnabled) AHObject.mouseEnabled = false;
		}

		var pRatio = frame.retina?(window.devicePixelRatio || 1):1;

		var that = this;
		that.cycle = cycle;
		that.decimals = decimals
		that.alwaysHighlight = alwaysHighlight; // will want to set as getter setter
		that.AHTime = AHTime;
		that.AHColor = AHColor;
		that.AHBorderWidth = AHBorderWidth;
		that.AHBorderPadding = AHBorderPadding;
		that.AHAlpha = AHAlpha;
		that.AHObjectScale = AHObjectScale;

		// ZIM Accessibility
		// If there is a tabOrder then make HTML tags (tabTags) that match tab objects
		// Put these outside viewable document area and give them titles that match the text property
		// ALSO - add:
		// 1. a tabPrefixTag before the canvas
		// 2. a tabBufferTag before and after the last tabTag
		// 3. a tabSuffixTag before and after the buffer
		// These are used when exiting or entering canvas tab system
		// When entering, the tab system forwards the focus to the first or last tab on stage
		// When exiting, the prefix or suffix is left active, the title is read
		// This allows the next tab to go to the normal tab order on the HTML page
		// The buffer tag lets us enter the canvas if the user goes back after exiting to the suffix tag
		// there is no need for a buffer tag for the prefix as we use the canvas tag itself
		// 4. We also add a talk tag with a role of alert
		// We change the innerHTML of this tag and it will be read without losing focus

		// item is used for an object from the tabOrder - with obj and title properties
		// obj is used when referencing the obj property of the item

		var _tabOrder = -1; // will be an array, but need to handle purposeful empty array
		var _tabIndex = -1;
		var tabTags = [];
		var tabEvents = [];
		var phrases = {RadioButtons:"option", Tabs:"tab", Pad:"key"};
		var currentHighlight;
		var canvasID = frame.canvas.id;
		var mobile = zim.mobile();
		var ariaCheck = false; // keeps track of whether we have checked aria
		var _aria = null; // keeps track of aria setting
		var ariaEvents = []; // [obj, event] - uses on
		var prefixTab;
		var suffixTab;
		var talkTag;
		var noAriaTabPrefix
		var noAriaTabSuffix
		var noAriaTabPrefix2
		var noAriaTabSuffix2
		var firstDelay = 100; // ms to let component changes take place for mobile
		var secondDelay = 150;
		var lastAriaTag;
		Object.defineProperty(this, 'tabOrder', {
			get: function() {
				return _tabOrder;
			},
			set: function(array) {

				// remove any previous tabTags and prefix/suffix and events
				if (prefixTab) prefixTab.removeEventListener("focus", clearAlert);
				if (suffixTab) suffixTab.removeEventListener("focus", clearAlert);
				for (var i=0; i<tabTags.length; i++) {
					if (tabTags[i].parentElement) tabTags[i].outerHTML = "";
				}
				tabTags = [];

				// remove any previous mousedown events added to capture focus
				var item;
				var obj;
				for (i=0; i<tabEvents.length; i++) {
					item = _tabOrder[i];
					if (item) tabEvents[i][0].off("mousedown", tabEvents[i][1]);
				}
				tabEvents = [];
				_tabOrder = [];
				var finalTabs = [];
				var lastMadeTag = frame.canvas;

				var subtotal = 0;
				for (var i=0; i<array.length; i++) {
					item = array[i];
					if (item.type == "HotSpots") {
						// for (var j=0; j<item.hotSpots.length; j++) {
						// 	addSpots.push(item.hotSpots[j].spot);
						// }
						array.splice.apply(array, [i, 1].concat(item.hotSpots));
						item = item.hotSpots[0];
					}
					if (item.type == "HotSpot") item = item.spot;
					if (item.constructor == {}.constructor) {
						obj = item.obj;
						if (!obj || !obj.getStage) continue; // must provide display object
						if (!item.title) item.title = getText(obj, i+subtotal);
					} else {
						obj = item;
						item = {obj:item, title:getText(item, i+subtotal)};
					}

					// these components we will split into individual tab items
					// we use the RadioButtons labels and the Tabs and Pad buttons
					// and there is a slight difference in phrasing - otherwise, they work the same
					if (obj.type == "RadioButtons" || obj.type == "Tabs" || obj.type == "Pad") {
						var splitAdd = [];
						subtotal--; // subtract 1 for the obj in array we replace
						for (var j=0; j<obj.buttons.length; j++) {
							subtotal++;
							var but = obj.buttons[j];
							var splitItem = {obj:but, title:item.title + " - " + phrases[obj.type] + ": " + ((but.text=="a" || but.text=="A") ? "eh" : but.text)}
							splitAdd.push(splitItem);
							but.zimTabParent = obj;
							but.zimTabParent.zimAccessibility = that;
							but.zimTabIndex = i+subtotal; // for click event
							but.tabIndex = j; // for seeing if pressed
							but.zimAccessibility = that;
							addExtraTitle(splitItem);
							lastMadeTag	= makeTabTag(canvasID+"Tab"+(i+subtotal), splitItem.title, lastMadeTag, splitItem);
							tabEvents.push([but, but.on("mousedown", setTabFocus)]);
							if (!ariaCheck) ariaEvents.push([but, but.on("mousedown", ariaCheckEvent)]);
						}
						finalTabs = finalTabs.concat(splitAdd);
					} else {
						obj.zimTabIndex = i+subtotal;
						obj.zimTabParent = obj; // just so always can ask for zimTabParent
						addExtraTitle(item);
						obj.zimAccessibility = that;
						tabEvents.push([obj, obj.on("mousedown", setTabFocus)]);
						if (!ariaCheck) ariaEvents.push([obj, obj.on("mousedown", ariaCheckEvent)]);
						finalTabs.push(item);
						lastMadeTag	= makeTabTag(canvasID+"Tab"+(i+subtotal), item.title, lastMadeTag, item);
					}
				}

				// ARIA
				// need to check for aria which is used by Apple Voice Over for instance...
				// so start assuming aria is true
				// then make a noAria tag before the prefix and a noAria tag after the suffix
				// these will have aria-hidden = true attributes
				// they also have a focus event and if the focus is triggered, _aria gets set to false
				// also, the noAria tags are removed and focus given to the prefix or suffix
				// So... the check does not work right away, but just as focus

				prefixTab = that.startAppTag = makeTabTag(canvasID+"PrefixTab", appName + " start", frame.canvas, null, "before"); // resting tag when exiting backwards
				prefixTab.addEventListener("focus", clearAlert);
				if (!ariaCheck) {
					prefixTab.addEventListener("focus", removeAriaEvent);
					// make tags surrounding prefix that set aria to false if seen by non-aria
					noAriaTabPrefix = makeTabTag(canvasID+"noAriaTab", "", prefixTab, null, "before");
					noAriaTabPrefix.setAttribute("aria-hidden", true);
					noAriaTabPrefix.focusTab = prefixTab;
					noAriaTabPrefix.addEventListener("focus", ariaFalseEvent);
					// if click inside on TextArea or Loader it will not determine aria
					// and so, we may arrive at the prefix going backwards from the inside - sigh
					noAriaTabPrefix2 = makeTabTag(canvasID+"noAriaTab", "", prefixTab);
					noAriaTabPrefix2.setAttribute("aria-hidden", true);
					noAriaTabPrefix2.focusTab = prefixTab;
					noAriaTabPrefix2.addEventListener("focus", ariaFalseEvent);
				}

				lastMadeTag = makeTabTag(canvasID+"BufferTab", "", lastMadeTag); // catch tag if sent to suffix then shift tabs back in to canvas
				// not sure if I need this on mobile
				if (mobile) lastMadeTag.setAttribute("aria-hidden", true);

				lastMadeTag = suffixTab = that.endAppTag = makeTabTag(canvasID+"SuffixTab", appName + " end", lastMadeTag); // resting tag when exiting forward
				suffixTab.addEventListener("focus", clearAlert);
				if (!ariaCheck) {
					suffixTab.addEventListener("focus", removeAriaEvent);
					lastMadeTag = noAriaTabSuffix = makeTabTag(canvasID+"noAriaTab", "", suffixTab); // resting tag when exiting backwards
					noAriaTabSuffix.setAttribute("aria-hidden", true);
					noAriaTabSuffix.focusTab = suffixTab;
					noAriaTabSuffix.addEventListener("focus", ariaFalseEvent);

					noAriaTabSuffix2 = makeTabTag(canvasID+"noAriaTab", "", suffixTab, null, "before"); // resting tag when exiting backwards
					noAriaTabSuffix2.setAttribute("aria-hidden", true);
					noAriaTabSuffix2.focusTab = suffixTab;
					noAriaTabSuffix2.addEventListener("focus", ariaFalseEvent);
				}

				_tabOrder = finalTabs;
			}
		});


		Object.defineProperty(this, 'tabIndex', {
			get: function() {
				return _tabIndex;
			},
			set: function(num) {
				if (!_state) return;
				if (num < 0 || num >= _tabOrder.length) {
					clearTab();
				} else {
					if (num != _tabIndex) clearOld();
					var obj = _tabOrder[num].obj;
					if (obj.stage) {
						_tabIndex = num;
						obj.focus = true;
						tabFocus = true;
						tabFirstCheck = false;

						that.changeTitle(obj);
						obj.zimTabTag.focus();

						setTimeout(function() {
							obj.zimTabTag.focus();
							var readerEvent = new createjs.Event("change");
							readerEvent.title = obj.zimTabTag.getAttribute("aria-label");
							that.dispatchEvent(readerEvent);
						}, secondDelay);

						// if (num == _tabIndex) return;

						// custom hightlights
						if (that.alwaysHighlight && !_aria) {
							if (!that.AHObject) {
								currentHighlight = alwaysHighlightShape;
								var oB = obj.getBounds();
								var pTL = obj.localToGlobal(oB.x-5, oB.y-5);
								var pTR = obj.localToGlobal(oB.x+oB.width+5, oB.y-5);
								var pBR = obj.localToGlobal(oB.x+oB.width+5, oB.y+oB.height+5);
								var pBL = obj.localToGlobal(oB.x-5, oB.y+oB.height+5);

								var g = alwaysHighlightShape.graphics;
								g.clear();
								g.s(that.AHColor).ss(that.AHBorderWidth)
									.mt(pTL.x, pTL.y)
									.lt(pTR.x, pTR.y)
									.lt(pBR.x, pBR.y)
									.lt(pBL.x, pBL.y)
									.lt(pTL.x, pTL.y)
									.cp();
								alwaysHighlightShape.alpha = that.AHAlpha;
								if (that.AHTime > 0) tabTimeout = setTimeout(function(){frame.stage.removeChild(alwaysHighlightShape); frame.stage.update();}, that.AHTime);

							} else {
								currentHighlight = that.AHObject;
								var b = zim.boundsToGlobal(obj);
								that.AHObject.alp(that.AHAlpha).addTo(frame.stage, null, false)
								that.AHObject.fit(b.x, b.y, b.width, b.height)
								that.AHObject.sca(that.AHObject.scaleX*that.AHObjectScale);
								if (that.AHTime > 0) tabTimeout = setTimeout(function(){frame.stage.removeChild(that.AHObject); frame.stage.update();}, that.AHTime);
							}
							frame.stage.addChild(currentHighlight);
							frame.stage.update();
						}
					} else {
					 	clearTab();
					}
				}
				function clearOld() {
					if (_tabIndex && _tabIndex > -1) {
						if (_tabOrder && _tabOrder[_tabIndex]) _tabOrder[_tabIndex].obj.focus = false;
					} else {
						for (var i=0; i<_tabOrder.length; i++) {
							_tabOrder[i].obj.focus = false;
						}
					}
				}
				function clearTab() {
					clearOld();
					tabFocus = false;
					if (that.AHObject) frame.stage.removeChild(that.AHObject);
					if (that.alwaysHighlightShape) frame.stage.removeChild(that.alwaysHighlightShape);
					frame.stage.update();
					_tabIndex = -1;
				}
			}
		});

		Object.defineProperty(this, 'aria', {
			get: function() {
				return _aria;
			},
			set: function(val) {
				_aria = val;
				if (_aria) setAriaTrue();
				// and more
			}
		});

		// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
		// Aria
		// function called from mousedown on any object to see if entering app through mousedown rather than tab
		// ONLY HAPPENS ONCE! function gets removed after checking
		function ariaCheckEvent(e) {
			var obj = e.currentTarget;
			var bounds = obj.boundsToGlobal();
			var cX = bounds.x + bounds.width/2;
			var cY = bounds.y + bounds.height/2;
			var delta = 5;
			// aira sends event through with mouseX and mouseY at the center of the object
			if (e.stageX/e.target.stage.scaleX < cX-delta || e.stageX/e.target.stage.scaleX > cX+delta || e.stageY/e.target.stage.scaleY < cY-delta || e.stageY/e.target.stage.scaleY > cY+delta) {
				setAriaFalse();
			} else {
				setAriaTrue();
			}
		}

		function clearAlert() {
			if (talkTag) talkTag.innerHTML = "";
		}

		// function to set aria true
		function setAriaTrue() {
			_aria = true;
			removeAriaCheck();
			if (talkTag) talkTag.setAttribute("aria-hidden", false);

			zim.ACTIONEVENT = "click";
			if (!zns) ACTIONEVENT = "click";
			// can't tell if aria until first click which might place a highlight
			// aria uses its own highlight so do not want two highlights
			// for windows narrator, if we use a custom highlight then we move the tags off the stage
			// but with mobile, they don't get moved off the stage but rather to the edge of the screen - arrg
			frame.stage.removeChild(currentHighlight);
			that.alwaysHighlight = false;
			frame.stage.update();
			var item;
			var obj;
			var tag;
			for (var i=0; i<_tabOrder.length; i++) {
				item = _tabOrder[i];
				obj = item.obj;
				tag = obj.zimTabTag;
				tag.disabled = false;
				item.title = item.title.replace(/\s\(use arrow keys\)/, "");
				tag.setAttribute("aria-label", item.title);
				tag.addEventListener("focus", ariaTagFocusEvent);
			}
		}
		// function to set aria false
		function setAriaFalse() {
			_aria = false;
			var item;
			var obj;
			var tag;
			for (var i=0; i<_tabOrder.length; i++) {
				item = _tabOrder[i];
				obj = item.obj;
				tag = obj.zimTabTag;
				tag.disabled = false;
			}
			removeAriaCheck();
			if (alwaysHighlight) moveTagsOffstage();
		}

		// function to go to new tag via aria swipe
		// note, the aria-label is read as soon as focus is received
		// so can't do any testing of selected, or values
		// so the label must be set before coming in to focus
		function ariaTagFocusEvent(e) {
			var tag = e.currentTarget;
			var obj = tag.zimObject;
			// that.tabIndex = obj.zimTabIndex;
			_tabIndex = obj.zimTabIndex;
			obj.focus = true;
			var readerEvent = new createjs.Event("change");
			readerEvent.title = tag.getAttribute("aria-label");
			that.dispatchEvent(readerEvent);
		}

		// function to send focus to prefix or suffix if no aria present
		function ariaFalseEvent(e) {
			_aria = false;
			e.target.focusTab.focus();
		}
		// function to set aria when arriving at prefix or suffix
		function removeAriaEvent() {
			if (zot(_aria) || _aria) setAriaTrue(); // was not set to false or was set to true
			else setAriaFalse(); // was set to false
		}

		// function to remove ariaChecking objects and events
		function removeAriaCheck() {
			ariaCheck = true;

			prefixTab.removeEventListener("focus", removeAriaEvent);
			suffixTab.removeEventListener("focus", removeAriaEvent);

			noAriaTabPrefix.removeEventListener("focus", ariaFalseEvent);
			noAriaTabSuffix.removeEventListener("focus", ariaFalseEvent);
			noAriaTabPrefix.parentNode.removeChild(noAriaTabPrefix);
			noAriaTabSuffix.parentNode.removeChild(noAriaTabSuffix);

			noAriaTabPrefix2.removeEventListener("focus", ariaFalseEvent);
			noAriaTabSuffix2.removeEventListener("focus", ariaFalseEvent);
			noAriaTabPrefix2.parentNode.removeChild(noAriaTabPrefix2);
			noAriaTabSuffix2.parentNode.removeChild(noAriaTabSuffix2);

			for (var i=0; i<ariaEvents.length; i++) {
				ariaEvents[i][0].off("mousedown", ariaEvents[i][1]);
			}
		}

		// function to move tags offstage if there is no aria
		function moveTagsOffstage() {
			for (var i=0; i<tabTags.length; i++) {
				var t = tabTags[i];
				if (t.zimObject && (t.zimObject.type == "Loader" || t.zimObject.type == "TextArea" || t.zimObject.type == "Tag")) continue;
				tabTags[i].style.left = "-2000px";
			}
		}


		// ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

		// function to handle tabOrder parameter versus tabOrder property being set at start
		if (!zot(tabOrder)) {
			_tabOrder = [];
			that.tabOrder = tabOrder;

			if (!zot(tabIndex)) that.tabIndex = tabIndex;
		} else {
			// wait a sec to see if the tabOrder property is set
			// and if not, set a default tabOrder to all objects on the stage
			setTimeout(function() {
				if (_tabOrder == -1) {
					_tabOrder = [];
					var tabTemp = [];
					zim.loop(frame.stage, function(obj){if (obj.type) tabTemp.push(obj);});
					that.tabOrder = tabTemp;
					if (!zot(tabIndex)) that.tabIndex = tabIndex;
				}
			}, 50);
		}

		// CLICK
		// function called from mousedown on any tabOrder objects to handle setting tabIndex
		function setTabFocus(e) {
			var obj = e.currentTarget;
			if (obj.type == "RadioButton" || obj.type == "TabsButton" || obj.type == "PadButton") {
				for (var j=0; j<obj.zimTabParent.buttons.length; j++) {
					var but = obj.zimTabParent.buttons[j];
					but.zimTabTag.setAttribute("aria-label", but.zimTabTag.getAttribute("aria-label").split(" - currently")[0] + (obj.zimTabParent.selectedIndex == but.tabIndex ? " - currently selected." : " - currently not selected."));
				}
			}
			that.tabIndex = e.currentTarget.zimTabIndex;
			that.activatedObject = obj;
		}

		// function to create text for screen reader prepending object type
		// and in some cases if tabParent then preparing prefix for future items like radio buttons
		function getText(obj) {
			if (obj.talk) return obj.talk;
			if (obj.type == "TextArea" || obj.type == "RadioButtons" || obj.type == "Tabs" || obj.type == "Pad") return obj.type; // first part of title naming for radio button if no title provided
			if (obj.type == "Waiter") return "Waiter active - please wait";
			return obj.text ? obj.type + " - " + ((obj.text=="a" || obj.text=="A") ? "eh" : obj.text) : obj.type + ((obj.type == "Dial" || obj.type == "Slider" || obj.type == "ColorPicker" || obj.type == "Stepper") ? " (use arrow keys)" : "") + ((obj.type == "TextArea") ? " (press ENTER to edit)" : "");
		}

		// function to add extra information to title
		function addExtraTitle(item) {
			var obj = item.obj;
			item.title = item.title.replace(/\.$/, "");
			if (obj.type == "RadioButton" || obj.type == "TabsButton" || obj.type == "PadButton") item.title = item.title + (obj.tabIndex == obj.zimTabParent.selectedIndex ? " - currently selected" : " - currently not selected");
			else if (obj.type == "CheckBox") item.title = item.title + (obj.checked ? " - currently checked" : " - currently not checked");
			else if (obj.type == "Stepper") item.title = item.title + " - currently displaying " + obj.currentValue;
			else if (obj.type == "Slider" || obj.type == "Dial") item.title = item.title + " - currently at " + zim.decimals(obj.currentValue, that.decimals);
			else if (obj.type == "ColorPicker") item.title = item.title + " - currently at " + obj.selectedColor;
			else if (obj.type == "TextArea") item.title = item.title + (obj.tag.value != "" ? "" : " - placeholder: " + obj.tag.placeholder); // text areas get read automatically (except placeholder)
			else if (obj.type == "Indicator") item.title = item.title + " - currently " + (obj.selectedIndex>=0 ? "at " + (obj.selectedIndex+1) + " of " + obj.num :  "not indicating");
			item.title += ".";
		}



		// function to make HTML tags off screen to feed titles to screen reader using focus()
		function makeTabTag(id, title, targetTag, item, place) {
			var obj;
			var tabTag;
			if (item) obj = item.obj;
			if (item && (obj.type == "TextArea" || obj.type == "Loader")) {
				var tabTag = obj.tag;
				tabTag.setAttribute("aria-label", title);
				if (obj) tabTag.setAttribute("aria-hidden", !obj.stage);
				if (place == "before") {
					targetTag.parentNode.insertBefore(tabTag, targetTag);
				} else {
					targetTag.parentNode.insertBefore(tabTag, targetTag.nextSibling);
				}
				tabTag.style.zIndex = -5;
				tabTag.zimObject = obj;
				item.obj.zimTabTag = tabTag;
				tabTags.push(tabTag);
				return tabTag;
			}
			if (item && (obj.type == "Dial" || obj.type == "Slider" || obj.type == "Stepper" || obj.type == "ColorPicker")) {
				var stepArray = [];
				if (obj.type == "Dial" || obj.type == "Slider") {
					var step = (obj.step <= 0 ? (obj.max-obj.min)/20 : obj.step);
					for (var i=obj.min; i<obj.max; i=i+step) {
						stepArray.push(obj.min + i);
					}
				} else if (obj.type == "Stepper") {
					stepArray = obj.stepperArray;
				} else if (obj.type == "ColorPicker") {
					stepArray = obj.colors;
				}
				tabTag = document.createElement("select");
				tabTag.disabled = true;
				tabTag.zimObject = obj;
				tabTag.addEventListener("change", function(e) {
					if (!_aria) return;
					e.currentTarget.zimObject.zimTabParent.currentValueEvent = e.target.value;
					frame.stage.update();
				});
				tabTag.size = 1;
				var el;
				var op;
				for (var i=0; i<stepArray.length; i++) {
					el = stepArray[i];
					op = document.createElement("option");
					op.setAttribute("aria-label", el);
					op.innerHTML = el;
					tabTag.add(op);
					if (el == obj.zimTabParent.currentValue) op.setAttribute("selected", "selected");
				}
				tabTag.setAttribute("role", "button");
			} else {
				tabTag = document.createElement("div");
				if (obj) tabTag.zimObject = obj;
				tabTag.innerHTML = "tag"; // needs to have text for ipad to read it
				tabTag.setAttribute("role", "button");
			}
			tabTag.setAttribute("id", id);
			tabTag.setAttribute("tabindex", 0);
			tabTag.setAttribute("aria-label", title);
			if (obj) tabTag.setAttribute("aria-hidden", !obj.stage);

			if (place == "before") {
				targetTag.parentNode.insertBefore(tabTag, targetTag);
			} else {
				targetTag.parentNode.insertBefore(tabTag, targetTag.nextSibling);
			}
			tabTag.style.position = "absolute";
			if (item) {
				var obj = item.obj;
				var bounds = obj.boundsToGlobal();
				zog("here")
				if (frame.retina) {
					tabTag.style.left = (frame.x + bounds.x*frame.scale/pRatio)+"px";
					tabTag.style.top = (frame.y + bounds.y*frame.scale/pRatio)+"px";
					tabTag.style.width = (bounds.width*frame.scale/pRatio)+"px";
					tabTag.style.height = (bounds.height*frame.scale/pRatio)+"px";
				} else {
					tabTag.style.left = (frame.x + bounds.x * frame.scale)+"px";
					tabTag.style.top = (frame.y + bounds.y * frame.scale)+"px";
					tabTag.style.width = (bounds.width * frame.scale)+"px";
					tabTag.style.height = (bounds.height * frame.scale)+"px";
				}
				item.obj.zimTabTag = tabTag;
			} else {
				tabTag.style.left = -1000+"px";
				tabTag.style.top = frame.y+"px";
			}
			tabTag.style.overflow = "hidden";
			tabTag.style.zIndex = -5;
			tabTag.style.fontSize = "20px";
			tabTag.style.color = "rgba(0,0,0,.01)";

			tabTags.push(tabTag);
			return tabTag;
		}

		// keydown event to handle tab, enter and arrows for various objects
		var tabFocus = false;
		var tabFirstCheck = true;
		var tabFrameEvent = frame.on("keydown", function(e) {
			if (e.keyCode==9) {
				if (!checkTabs()) { // check focus has not been given outside frame canvas
					tabFocus = false;
					if (that.tabIndex != -1) that.tabIndex = -1;
				}
				if (tabFocus) {
					goTabs(e);
				} else {
					setTabs(e);
				}
			}

			// area for special keys ENTER, ARROWS if frame has tabFocus, etc.
			if (tabFocus && _tabOrder.length > 0 && that.tabIndex >= 0) {

				// ENTER key
				if (e.keyCode==13) {
					var item = _tabOrder[that.tabIndex];
					var obj = item.obj;
					if (item && obj.stage) {
						var downEvent = new createjs.Event("mousedown");
						var clickEvent = new createjs.Event("click");
						downEvent.fromEnter = clickEvent.fromEnter = true;
						if (obj.type == "Pane") {
							obj.backdrop.dispatchEvent(downEvent); // on backdrop
							obj.backdrop.dispatchEvent(clickEvent);
						} else {
							obj.dispatchEvent(downEvent);
							obj.dispatchEvent(clickEvent);
						}
					}
				}

			} // end special keys (not tab)

		}); // keydown event


		// function to check if any other HTML tag or other Frame currently has taken focus
		// this is not called immediately when focus is lost but rather on next keydown event with tab key pressed
		function checkTabs() {
			var frameTagFocus = false;
			for (var i=0; i<tabTags.length; i++) {
				if (document.activeElement == tabTags[i]) {
					frameTagFocus = true;
					break;
				}
			}
			if (document.activeElement == frame.canvas || frameTagFocus) return true;
			return false;
		}

		// function to test for tab key event when currently on PrefixTab or SuffixTab for frame
		// will then set up tabFocus and send focus to right object with goTabs()
		function setTabs(e) {
			if (!frame.shiftKey && (zid(canvasID+"PrefixTab") == document.activeElement || frame.canvas == document.activeElement)) {
				// this means tabbing forward and arriving at canvas
				// set tab to last tabTag on canvas so next goes to first tabTag
				if (_tabOrder.length > 0) _tabOrder[_tabOrder.length-1].obj.focus = true;
				tabFirstCheck = true;
				tabFocus = true;
				goTabs(e);
			} else if (frame.shiftKey && (zid(canvasID+"SuffixTab") == document.activeElement || zid(canvasID+"BufferTab") == document.activeElement)) {
				// this means tabbing with shift and arriving at SuffixTab
				// so set to first tabTag so goTabs loops backwards around to last tabTag
				// as long as tabFirstCheck is true
				// we have entered the canvas so set the tabFocus to true
				if (_tabOrder.length > 0) _tabOrder[0].obj.focus = true;
				tabFirstCheck = true;
				tabFocus = true;
				goTabs(e);
			}
		}

		// function to call tab() used by setTabs and keydown when tabs are already set
		function goTabs(e) {
			if (e.ctrlKey) {
				focusToDoc(e.shiftKey?-1:1);
				return;
			}
			if (e.shiftKey) that.tab(-1);
			else that.tab(1);
			e.preventDefault();
		}

		// method to move to next or previous tab - called from goTabs() or by user
		var tabTimeout;
		this.tab = function(dir) {
			clearTimeout(tabTimeout);
			if (currentHighlight) frame.stage.removeChild(currentHighlight);
			if (zot(dir)) dir = 1;
			if (_tabOrder.length == 0) {
				focusToDoc(dir);
				return;
			}
			for (var i=0; i<_tabOrder.length; i++) {
				var obj = _tabOrder[i].obj;
				if (obj.focus) {
					obj.focus = false;
					var index = i + dir;
					var normalizedIndex = (index+_tabOrder.length*10000)%_tabOrder.length;
					var tabData = _tabOrder[normalizedIndex]
					obj = tabData.obj;

					// make sure on stage
					var attempts = 0;
					var badTabs = false;
					// keep looking if the object t is not on the stage or is not enabled (or parent is not enabled if it has one)
					while(!(obj.stage && ((!obj.zimTabParent && (zot(obj.enabled) || obj.enabled)) || (obj.zimTabParent && (zot(obj.zimTabParent.enabled) || obj.zimTabParent.enabled))))) {
						attempts++;
						index = index + dir;
						normalizedIndex = (index+_tabOrder.length*100)%_tabOrder.length;
						tabData = _tabOrder[normalizedIndex];
						obj = tabData.obj;
						if (attempts == _tabOrder.length) { // none on stage
							badTabs = true;
							break;
						};
					}
					if (badTabs || (!that.cycle && index != normalizedIndex && !tabFirstCheck)) {
						focusToDoc(dir);
						return;
					}
					tabFirstCheck = false;
					that.tabIndex = normalizedIndex;
					break;
				}
			}
		}

		// function to give focus to html doc when no more frame tabs left in direction we are tabbing
		function focusToDoc(dir) {
			tabFocus = false
			zid(canvasID + (dir==1 ? "SuffixTab" : "PrefixTab") ).focus();
			frame.stage.removeChild(AHObject);
			frame.stage.update();
			that.tabIndex = -1;
		}

		// target is tabIndex or obj
		this.changeTitle = function(target, title, activate) {
			if (typeof target != "number") target = target.zimTabIndex;
			if (zot(target)) return;
			if (activate && zot(title)) {that.tabIndex = target; return;} // changeTitle is called from within tabIndex setter function (without the activate part...)
			var item = _tabOrder[target];
			var obj = item.obj;
			// when passing no title will be coming from component for title update due to change in component
			// if you want no title, then pass "" as title
			if (zot(title)) title = obj.zimTabTag.getAttribute("aria-label").split(" - currently")[0];
			item.title = title;
			addExtraTitle(item);
			obj.zimTabTag.setAttribute("aria-label", item.title);
			if (activate) that.tabIndex = target;
		}
		var talkCheck = false;
		this.talk = function(words) {
			// talk tag
			// this prevents talk tag from being read at the start - activate it once we need to talk
			// then we set the innerHTML to "" when we swipe focus to start or end app on the way out
			// we may still get the tag read on mobile if they exit the app through some other tag
			// and then swipe focus towards end of app - as it is just after the end of app
			// this will then re-read the message... but it is unlikely, and did not see a way around it
			if (!talkCheck) {
				talkTag = makeTabTag(canvasID+"TalkTab", "", suffixTab);
				talkTag.setAttribute("role", "alert");
				// talkTag.setAttribute("aria-hidden", !_aria);
				talkCheck = true;
			}
			// talk tag is role alert which allows it to talk without removing focus
			// the tag is aria-hidden for aria == false
			talkTag.setAttribute("aria-label", words);
			talkTag.innerHTML = " "; // force change
			talkTag.innerHTML = words;
			zog(talkTag.innerHTML)
			var readerEvent = new createjs.Event("change");
			// TODO - remove talk in front of event
			readerEvent.title = words;
			that.dispatchEvent(readerEvent);
		}

		this.resize = function(target) {
			if (!zot(target)) {
				if (typeof target != "number") target = target.zimTabIndex;
				if (target < 0) return;
				var item = _tabOrder[target];
				var obj = item.obj;
				if (!obj.stage && currentHighlight && obj==that.currentObject) {
					frame.stage.removeChild(currentHighlight);
					frame.stage.update();
				}
				if (that.alwaysHighlight && !zot(_aria)) return;
				resizeObj(obj);
			} else {
				if (that.alwaysHighlight && !zot(_aria)) return;
				for (var i=0; i<_tabOrder.length; i++) {
					resizeObj(_tabOrder[i].obj);
				}
			}
			function resizeObj(obj) {
				if (obj.type == "TextArea" || obj.type == "Loader") {
					obj.resize();
					return;
				} else {
					var bounds = obj.boundsToGlobal();
					var tabTag = obj.zimTabTag;
					if (frame.retina) {
						zog("hjere")
						tabTag.style.left = (frame.x + bounds.x*frame.scale/pRatio)+"px";
						tabTag.style.top = (frame.y + bounds.y*frame.scale/pRatio)+"px";
						tabTag.style.width = (bounds.width*frame.scale/pRatio)+"px";
						tabTag.style.height = (bounds.height*frame.scale/pRatio)+"px";
					} else {
						tabTag.style.left = (frame.x + bounds.x * frame.scale/pRatio/pRatio)+"px";
						tabTag.style.top = (frame.y + bounds.y * frame.scale/pRatio/pRatio)+"px";
						tabTag.style.width = (bounds.width * frame.scale/pRatio/pRatio)+"px";
						tabTag.style.height = (bounds.height * frame.scale/pRatio/pRatio)+"px";
					}
				}
				tabTag.setAttribute("aria-hidden", !obj.stage);
				tabTag.hidden = !obj.stage;
			}
		}

		Object.defineProperty(this, 'currentObject', {
			get: function() {
				if (_tabOrder[_tabIndex] && _tabOrder[_tabIndex].obj) return _tabOrder[_tabIndex].obj;
				else return null
			},
			set: function(obj) {
				for (var i=0; i<_tabOrder.length; i++) {
					if (_tabOrder[i].obj == obj) {that.tabIndex = i; break;}
				}
			}
		});

		_state = true;
		Object.defineProperty(this, 'enabled', {
			get: function() {
				return _state;
			},
			set: function(state) {
				_state = state;
			}
		});

		this.dispose = function() {
			that.tabOrder = [];
			for (var i=0; i<tabTags.length; i++) if (tabTags[i].parentElement) tabTags[i].outerHTML = "";
			that.removeAllEventListeners();
			frame.off("keydown", tabFrameEvent);
		}
	}
	zim.extend(zim.Accessibility, createjs.EventDispatcher, null, "cjsEventDispatcher", false);
	//-30.5

// SUBSECTION MANAGERS

/*--
zim.Manager = function()

Manager
zim class

DESCRIPTION
used internally to make ResizeManager, GridManager and GuideManager
and in future perhaps OutlineManager
--*///+75
	zim.Manager = function(type) {
		z_d("75");
		var that = this;
		this.type = type;
		this.items = [];
		this.add = function(obj) {
			if (Array.isArray(obj)) that.items = that.items.concat(obj);
			else that.items.push(obj);
		}
		this.remove = function(obj) {
			if (zot(obj)) {that.items = []; return;}
			if (!Array.isArray(obj)) obj = [obj];
			var o;
			for (var i=0; i<obj.length; i++) {
				o = obj[i];
				var index = that.items.indexOf(o);
				if (index != -1) that.items.splice(index, 1);
			}
		}
		this.resize = function() {
			if (!that) return;
			for (var i=0; i<that.items.length; i++) {
				if (!that.items[i].resize) that.items.splice(i); // was disposed
				else that.items[i].resize();
			}
		}
		this.toggle = function(state) {
			if (!that) return;
			for (var i=0; i<that.items.length; i++) {
				if (!that.items[i].toggle) that.items.splice(i); // was disposed
				else that.items[i].toggle(state);
			}
		}
		this.dispose = function() {
			for (var i=that.items.length-1; i>=0; i--) {
				that.items[i].dispose();
			}
			that.items = [];
			that = null;
			return true;
		}
	}//-75

/*--
zim.ResizeManager = function()

ResizeManager
zim class extends zim.Manager abstract class

DESCRIPTION
Add objects with a resize() method to a ResizeManager object and call a single resize() on the ResizeManager object
This will most likely go in a resize event on the Frame
Works with objects such as Layout, Pages, Grid, Guide, Accessibility, Loader and TextArea

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var resizeManager = new ResizeManager();
resizeManager.add([pages, layout, accessibility]);
// where these three objects have already been made
// *** Note that the Loader and TextArea are already resized if added to an Accessibility object that is resized
frame.on("resize", function() {
	resizeManager.resize(); // without ResizeManager you would make three different resize() calls
})
END EXAMPLE

METHODS
add(obj) - adds object or an array of objects to the ResizeManager
	*** Note that the Loader and TextArea are already resized if added to an Accessibility object that is resized
remove(obj) - removes object or an array of objects from the ResizeManager
resize() - calls the resize() method of any object in the ResizeManager
dispose() - disposes the objects in the ResizeManager and the ResizeManager itself

PROPERTIES
type - holds the class name as a String
items - get or set an array of objects currently in the Manager

--*///+75.5
	zim.ResizeManager = function() {
		z_d("75.5");
		zim.Manager.call(this, "ResizeManager");
	}
	zim.ResizeManager.prototype = new zim.Manager();
	zim.ResizeManager.prototype.constructor = zim.ResizeManager;
	//-75.5

/*--
zim.TransformManager = function(objects, persistID)

TransformManager
zim class extends CreateJS EventDispatcher

DESCRIPTION
Manages multiple objects with transform() methods set.
Can use to show, hide, hideAll, add, remove and resize transform controls.
Can be used to automatically save any transforms and reload them again on refresh of Browser / App.
This uses localStorage.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var rect = new Rectangle(300, 200, green)
	.centerReg(stage)
	.mov(-200)
	.transform();

var circ = new Circle(100, red)
	.centerReg(stage)
	.mov(200)
	.transform();

var tm = new TransformManager([rect, circ], "sample");
// or use methods:
// tm.add([rect, circ]);
// tm.persist("sample"); // or add later to save transforms
END EXAMPLE

PARAMETERS
objects - (default null) adds the object(s) to the Transform Manager
	pass in a single object or an array of multiple objects
persistID - (default null) String id to make ZIM remember transforms of objects (uses localStorage)

METHODS
add(obj) - adds object or an array of objects to the TransformManager
remove(obj) - removes object or an array of objects to the TransformManager
show(obj) - show controls for an object that has a transform() set
hide(obj) - hides controls for an object that has a transform() set - still available with click
hideAll() - hides all controls - still available with click
resize() - calls the resize() method of any object in the ResizeManager
persist(id) - save data after every change and reload transforms when done - must provide an id
clearPersist(id) - clear persisting data - do this before adding shapes - must provide an id
savePersist() - with persist() already set, this will force a saving even without a transform event being captured
	if resize() after non-transform movement is called, then this is not needed
stopPersist() - no longer save data
dispose(removePersist, removeControls) - default just removes manager - keeps the data
	set removePersist to true to remove the persist data
	set removeControls to true to remove the transforms and beziers of all the items

PROPERTIES
type - holds the class name as a String
items - get or set (set not recommended) an array of objects currently in the TransformManager
currentObject - the last item to get transform tools if it still has the transform tools active
persistData - gets the persist data if it exists

EVENTS
Dispatches a "transformed" event when pressup on any of the controls or on click
	transformed event object has transformObject and transformType properties
	the transformType property has values of:
	 	FOR TRANSFORM: "size", "move", "rotate", "stretch", "reg" "reset"
	 	FOR BLOB, SQUIGGLE: "move", "bezierPoint", "bezierHandle", "bezierSwitch"
Adds "transformshow" and "transformhide" events for when click to hide or show controls
	these have a transformObject property to indicate what was shown or hidden
Dispatches, on each object, a "persistset" event when persist data is ready - could be delayed 50ms due to Layer being created
Dispatches a "persistcomplete" event when all persist data has been set + 100ms for layers to be set before applying controls

--*///+75.7
	zim.TransformManager = function(objects, persistID) {
		z_d("75.7");
		var that = this;
		this.type = "TransformManager";
		this.items = [];
		function setPersist(e) {
			if (!localStorage || (e && !e.pressup)) return;
			that.savePersist(e);
		}
		var tmChangeEvent;
		this.add = function(obj) {
			var newObj = []
			if (Array.isArray(obj)) {newObj = obj;} else {newObj = [obj];}
			var o; var firstEver = false;
			for (var i=0; i<newObj.length; i++) {
				o = newObj[i];
				if (that.items.length == 0 && i == 0) firstEver = true;
				if (o.type == "Blob" || o.type == "Squiggle") {
					if (!that.persistID) {
						if (firstEver) {
							firstEver = false;
							o.controlsVisible = true;
							that.currentObject = o;
						} else {
							o.controlsVisible = false;
						}
					}
					o.on("change", function(e) {
						var ev = new createjs.Event("transformed");
						ev.transformObject = e.target;
						ev.transformType = e.controlType;
						that.dispatchEvent(ev);
					});
					o.on("controlsshow", function(e) {
						var ev = new createjs.Event("transformshow");
						that.currentObject = ev.transformObject = e.target;
						that.dispatchEvent(ev);
					});
					o.on("controlshide", function(e) {
						var ev = new createjs.Event("transformhide");
						ev.transformObject = e.target;
						that.currentObject = null;
						that.dispatchEvent(ev);
					});
					o.on("update", function (e) {
						var ev = new createjs.Event("transformed");
						ev.transformObject = e.target;
						ev.transformType = e.controlType;
						that.dispatchEvent(ev);
					});
				} else {
					transObj = o.transformControls;
					if (!that.persistID) {
						if (firstEver && o.type != "Layer") {
							firstEver = false;
							if (transObj && transObj.visible) transObj.show();
							that.currentObject = o;
						} else {
							if (transObj) transObj.hide();
						}
					}
					o.on("transformed", function(e) {
						var ev = new createjs.Event("transformed");
						ev.transformObject = e.target;
						ev.transformType = e.transformType;
						that.dispatchEvent(ev);
					});
					o.on("transformshow", function(e) {
						var ev = new createjs.Event("transformshow");
						that.currentObject = ev.transformObject = e.target;
						that.dispatchEvent(ev);
					});
					o.on("transformhide", function(e) {
						var ev = new createjs.Event("transformhide");
						ev.transformObject = e.target;
						that.currentObject = null;
						that.dispatchEvent(ev);
					});
				}

			}
			that.items = that.items.concat(obj);
		}
		this.remove = function(obj) {
			if (zot(obj)) {that.items = []; return;}
			if (!Array.isArray(obj)) obj = [obj];
			var o;
			for (var i=0; i<obj.length; i++) {
				o = obj[i];
				if (that.currentObject == o) that.currentObject = null;
				if (o.transformedEvent) {
					o.off("transformed", o.transformedEvent);
					o.transformedEvent = null;
				}
				if (o.type == "Blob" || o.type == "Squiggle") o.controlsVisible = false;
				else o.transformControls.remove();
				var index = that.items.indexOf(o);
				if (index != -1) that.items.splice(index, 1);
			}
			if (persistID) {
				that.savePersist();
			}
		}
		this.show = function(obj) {
			if (zot(obj)) return;
			if (obj.type == "Blob" || obj.type == "Squiggle") {
				if (!obj.controlsVisible) obj.controlsVisible = true;
			} else {
				var transObj = obj.transformControls;
				if (!transObj || transObj.visible) return;
				transObj.show();
			}
			that.currentObject = obj;
		}
		this.hide = function(obj) {
			if (zot(obj)) return;
			if (that.currentObject == obj) that.currentObject = null;
			if (obj.type == "Blob" || obj.type == "Squiggle") {
				if (obj.controlsVisible) obj.controlsVisible = false;
			} else {
				var transObj = obj.transformControls;
				if (!transObj || transObj.visible) return;
				transObj.hide();
			}
		}
		this.hideAll = function(exception) {
			var transObj;
			var o;
			for (var i=0; i<that.items.length; i++) {
				o = that.items[i];
				if (!o || !(o.transformControls || o.update)) {
					that.items.splice(i); // was disposed
				} else {
					if (that.items[i]==exception) continue;
					if (o.type == "Blob" || o.type == "Squiggle") {
						if (o.controlsVisible) o.controlsVisible = false;
					} else {
						transObj = that.items[i].transformControls;
						if (transObj.visible) transObj.hide();
					}
				}
			}
			that.currentObject = null;
		}
		this.resize = function() {
			if (!that) return;
			var o;
			for (var i=0; i<that.items.length; i++) {
				o = that.items[i];
				if (!o || !(o.transformControls || o.update)) {
					that.items.splice(i); // was disposed
				} else {
					if (o.type == "Blob" || o.type == "Squiggle") {
						o.update();
					} else {
						o.transformControls.resize();
					}
				}
			}
			if (persistID) that.savePersist();
		}

		if (!zot(objects)) that.add(objects);
		var sorry = "TransformManager persist(persistID) - sorry, must provide id";
		this.persist = function(persistID) {
			if (zot(persistID)) {
				if (zon) {zog(sorry); return;}
			}
			that.persistID = persistID;
			var dispatchNum = 0;
			if (localStorage && localStorage[persistID]) {
				var data = that.persistData = JSON.parse(localStorage[persistID]);
				if (data.length == that.items.length) {
					for (var i=0; i<that.items.length; i++) {
						if (data[i].controlsVisible) { // check transform rather than blob or squiggle - anything else have controls?
							that.currentObject = that.items[i];
						}
						if (data[i] && (data[i].type == "Blob" || data[i].type == "Squiggle")) {
							if (that.items[i].points.length != data[i].points.length) {
								that.items[i].points = data[i].points;
							}
							that.items[i].setData(data[i]);
						} else if (data[i]) {
							if (zot(that.items[i].transformControls)) {
								-function(){
									var it = that.items[i];
									var da = data[i];
									interval(50, function(o) {
										if (it.transformControls) {
											it.transformControls.setData(da);
											it.dispatchEvent("persistset");
											dispatchNum++;
											if (dispatchNum == data.length) {
												setTimeout(function(){
													that.dispatchEvent("persistcomplete");
												}, 100);
											}
											o.clear();
										}
									}, 20);
								}();
							} else {
								that.items[i].transformControls.setData(data[i]);
								-function(){
									var it = that.items[i];
									timeout(50, function(o) {
										it.dispatchEvent("persistset");
										dispatchNum++;
										if (dispatchNum == data.length) {
											setTimeout(function(){
												that.dispatchEvent("persistcomplete");
											}, 100);
										}
									});
								}();
							}
						}
					}
				}
			}
			var it;
			for (var i=0; i<that.items.length; i++) {
				it = that.items[i];
				if (!it.transformedEvent) {
					if (it.type == "Blob" || it.type == "Squiggle") {
						it.transformedEvent = it.on("change", setPersist);
						it.transformedEvent = it.on("update", setPersist);
						it.transformedEvent = it.on("controlsshow", setPersist);
						it.transformedEvent = it.on("controlshide", setPersist);
					} else {
						it.transformedEvent = it.on("transformed", setPersist);
						it.transformedEvent = it.on("transformshow", setPersist);
						it.transformedEvent = it.on("transformhide", setPersist);
					}
				}
			}
		}
		this.savePersist = function(e) {
			var data = [];
			for (var i=0; i<that.items.length; i++) {
				it = that.items[i];
				if (it.type == "Blob" || it.type == "Squiggle") {
					if (it.recordData) data.push(it.recordData());
				} else {
					if (it.transformControls && it.transformControls.recordData) data.push(it.transformControls.recordData());
				}
			}
			that.persistData = data;
			localStorage[that.persistID] = JSON.stringify(data);
		}
		this.clearPersist = function(id) {
			if (zot(id)) {
				if (zon) {zog(sorry); return;}
			}
			that.persistData = null;
			if (localStorage) localStorage.removeItem(id);
		}
		this.stopPersist = function() {
			for (var i=0; i<that.items.length; i++) {
				it = that.items[i];
				if (it.transformedEvent) {
					if (it.type == "Blob" || it.type == "Squiggle") {
						it.off("change", it.transformedEvent);
					} else {
						it.off("transformed", it.transformedEvent);
					}
					it.transformedEvent = null;
				}
			}
			that.persistData = null;
		}
		if (!zot(persistID)) that.persist(persistID);

		this.dispose = function(removePersist, removeControls) {
			if (zot(removePersist)) removePersist = true;
			that.removeAllEventListeners();
			if (that.persistID && removePersist) that.stopPersist();
			if (removeControls) {
				for (var i=0; i<that.items.length; i++) {
					o = that.items[i];
					if (o.type == "Blob" || o.type == "Squiggle") o.dispose();
					else o.transformControls.dispose();
				}
			}
		}
	}
	zim.extend(zim.TransformManager, createjs.EventDispatcher, null, "cjsEventDispatcher", false);
	//-75.7

/*--
zim.GuideManager = function()

GuideManager
zim class - extends the ZIM Manager abstract class

DESCRIPTION
Add Zim Guide objects to a GuideManager object and update or remove all guides at once.
Guides are handy to use but perhaps annoying to update and remove if you have many.
GuideManager keeps track of the guides and lets you update or dispose of them on command.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var manager = new GuideManager();
manager.add(new Guide());
manager.add(new Guide(false));

// or with pixels
// manager.add(new Guide(true, false));
// manager.add(new Guide(false, false));

// then you can remove all guides with
// manager.dispose();
// handy with guides on multiple Pages

// and in frame resize event we can resize all guides:
frame.on("resize", function() {manager.resize();})
END EXAMPLE

PROPERTIES
type - holds the class name as a String
items - an array of all Guide objects added with add()

METHODS
add(guide) - registers a guide with the GuideManager
remove(guide) - removes guide from register
resize() - resizes all the guides in the GuideManager (ie. if stage changes)
toggle(state - default null) - toggle() will show controls if they are hidden or hide controls if they are showing
	alternatively, pass in true to show controls or false to hide controls
	note - method does not update the stage
dispose() - disposes all guides and the GuideManager

NOTE: to just hide guides, you use the G key
and to toggle percent and pixels use the P key
you can dispose guides individually or use this class to dispose all
disposing will remove the G, P key listener and the guide
--*///+77
	zim.GuideManager = function() {
		z_d("77");
		zim.Manager.call(this, "GuideManager");
	}
	zim.GuideManager.prototype = new zim.Manager();
	zim.GuideManager.prototype.constructor = zim.GuideManager;
	//-77

/*--
zim.GridManager = function()

GridManager
zim class - extends a zim.Manager

DESCRIPTION
Add Zim Grid objects to a GridManager object and update or remove all grids at once.
Grids are handy to use but perhaps annoying to update and remove if you have many.
GridManager keeps track of the grids and lets you update or dispose of them on command.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var manager = new GridManager();
manager.add(new Grid());

// or with pixels
// manager.add(new Grid(null, false));

// then you can remove all grids with
// grid.dispose();
// handy with guides on multiple Pages

// and in frame resize event we can resize all grids:
frame.on("resize", function() {manager.resize();})
END EXAMPLE

METHODS
add(grid) - registers a grid with the GridManager
remove(grid) - removes grid from the register
resize() - resizes all the grids in the GridManager (ie. if stage changes)
dispose() - disposes all grids and the GridManager

NOTE: to just hide grids, you use the G key
and to toggle percent and pixels use the P key
you can dispose grids individually or use this class to dispose all
disposing will remove the G key listener and the grid

PROPERTIES
type - holds the class name as a String
items - an array of all Grid objects added with add()
--*///+79
	zim.GridManager = function() {
		z_d("79");
		zim.Manager.call(this, "GridManager");
	}
	zim.GridManager.prototype = new zim.Manager();
	zim.GridManager.prototype.constructor = zim.GridManager;
	//-79

/*--
zim.LayoutManager = function()

LayoutManager
zim class

DESCRIPTION
Add Zim Layout objects to a LayoutManager object and update them all at once.
You can remove all layout region bound shapes at once
as well as remove the B key to show the region bound shapes.
For a final project, call the dispose().
This will remove all shapes and key events.
The layouts will remain in place to handle multiple screen sizes.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// these would be containers with your content
// make sure that bounds are set on containers
// you may want to hard code bounds for clarity
var header = new Rectangle(500, 200, "blue");
var content = new Rectangle(600, 500, "green");
var footer = new Rectangle(500, 200, "blue");
stage.addChild(header, content, footer);

// make the Layout - more useful for FULL scale mode
var layout = new Layout({
	holder:stage,
	regions:[
		{object:header, marginTop:10, maxWidth:80, minHeight:10, valign:"top"},
		{object:content, marginTop:5, maxWidth:90}, // note, middle gets no minHeight
		{object:footer, marginTop:5, maxWidth:80, height:10}
	],
	lastMargin:5
});

// add to LayoutManager to resize or dispose all layouts together
// disposing only removes keyboard events to show boundaries
var manager = new LayoutManager();
manager.add(layout);

frame.on("resize", function() {
	manager.resize();
	stage.update();
});

stage.update();
END EXAMPLE

METHODS
add(layout) - registers a layout with the LayoutManager
resize() - resizes all the layouts in the LayoutManager
disable() - disables all the layouts in the LayoutManager (shapes and sizing)
enable() - enables all the layouts in the LayoutManager (shapes and sizing)
dispose() - only removes bounds shapes and keyboard events (does not really dispose)

NOTE: to just hide bounds, you use the B key

PROPERTIES
type - holds the class name as a String
items - an array of all Layout objects added with add()
--*///+81
	zim.LayoutManager = function() {
		z_d("81");
		this.type = "LayoutManager";
		var that = this;
		this.items = [];
		this.add = function(layout) {
			that.items.push(layout);
		}
		this.resize = function() {
			for (var i=0; i<that.items.length; i++) {
				that.items[i].resize();
			}
		}
		this.disable = function() {
			for (var i=0; i<that.items.length; i++) {
				that.items[i].disable();
			}
		}
		this.enable = function() {
			for (var i=0; i<that.items.length; i++) {
				that.items[i].enable();
			}
		}

		this.dispose = function() {
			for (var i=0; i<that.items.length; i++) {
				that.items[i].removeShape(); // also removes key events
			}
			return true;
		}
	}//-81

/*--
zim.SelectionSet = function(selections)

SelectionSet
zim class

DESCRIPTION
Keeps track of selected objects or indexes, etc.
Just a wrapper for array manipulation:
Uses isSelected instead of indexOf()>=0
Uses remove() instead of splice(index, 1), etc.
Handles multiple select, matching other SelectionSet objects for add and remove, etc.
Use with a SelectionManager to control multiple Selection Set objects at once.
See: https://zimjs.com/explore/selectionTest.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var selectedTriangles = new SelectionSet();
var triangles = new Tile(new Triangle(), 5, 1, 50).center().mov(0,-200).cur();
triangles.on("click", function (e) {
	selectedTriangles.toggle(e.target, frame.ctrlKey); // multiple if ctrl down
	// need to loop through all to update
	triangles.loop(function (triangle) {
		triangle.color = selectedTriangles.isSelected(triangle)?pink:black;
	});
	stage.update();
});
END EXAMPLE

PARAMETERS
selections - (default []) an array of selected items or indexes, etc.

METHODS
clear() - clear selections
isSelected(item) - returns true if item is selected else false
toggle(item, multiple, match, unmatch) - toggle the item which calls add or remove below
	this is probably the most handy method - see parameter description below
add(item, multiple, match, unmatch) - add an item
	multiple will not remove other selected items
	match - pass in another SelectionSet to add the same item or index
	unMatch - pass in another SelectionSet to remove the same item or index if there
remove(item, multiple, match, unmatch) - remove an item
	multiple will not remove other selected items
	match - pass in another SelectionSet to remove the same item or index
	unMatch - pass in another SelectionSet to add the same item or index if there
dispose() -  clears list and sets to null

PROPERTIES
type - holds the class name as a String
items - an array of all Layout objects added with add()
--*///+81.5
	zim.SelectionSet = function(selections) {
		z_d("81.5");
		this.type = "SelectionSet";
		if (zot(selections)) selections = [];
		this.selections = selections;
		this.clear = function() {
			this.selections = [];
		}
		this.isSelected = function(item) {
			return this.selections.indexOf(item) >= 0;
		}
		// add will append if multiple or SelectionManager multiple is true
		// otherwise turns all other selections off and adds parameter selection
		this.add = function(item, multiple, match, unmatch) {
			if (zot(item)) return;
			if (zot(multiple)) {
				if (!zot(this.selectionManager)) multiple = this.selectionManager.multiple;
			}
			if (!zot(this.selectionManager)) {
				this.selectionManager.setCurrent(this);
				if (!multiple && this.selectionManager.multipleSets) this.selectionManager.clear();
			}
			if (!multiple ) this.clear();
			var ind = this.selections.indexOf(item);
			if (ind >= 0) return;
			this.selections.push(item);
			if (!zot(match)) {
				if (!Array.isArray(match)) match = [match];
				for (var i=0; i<=match.length; i++) {
					match[i].add(item, multiple);
				}
			}
			if (!zot(unmatch)) {
				if (!Array.isArray(unmatch)) unmatch = [unmatch];
				for (i=0; i<=unmatch.length; i++) {
					unmatch[i].remove(item, multiple);
				}
			}
		}
		this.remove = function(item, multiple, match, unmatch) {
			if (zot(item)) return;
			if (zot(multiple)) {
				if (!zot(this.selectionManager)) multiple = this.selectionManager.multiple;
			}
			if (!zot(this.selectionManager)) this.selectionManager.setCurrent(this);
			if (!multiple) this.clear();
			var ind = this.selections.indexOf(item);
			if (!zot(this.selectionManager)) {
				this.selectionManager.setCurrent(this);
				if (!multiple && this.selectionManager.multipleSets) this.selectionManager.clear();
			}
			if (!multiple ) this.clear();
			if (!multiple) this.clear();
			else if (ind >= 0) this.selections.splice(ind, 1);
			if (!zot(match)) {
				if (!Array.isArray(match)) match = [match];
				for (var i=0; i<=match.length; i++) {
					match[i].remove(item, multiple);
				}
			}
			if (!zot(unmatch)) {
				if (!Array.isArray(unmatch)) unmatch = [unmatch];
				for (i=0; i<=unmatch.length; i++) {
					unmatch[i].add(item, multiple);
				}
			}
		}
		this.toggle = function(item, multiple, match, unmatch) {
			if (this.isSelected(item)) this.remove(item, multiple, match, unmatch);
			else this.add(item, multiple, match, unmatch);
		}
		this.dispose = function() {
			this.clear();
			this.selections = null;
			return true;
		}
	}//-81.5

/*--
zim.SelectionManager = function(sets, multipleKey, multipleSets)

SelectionManager
zim class extends a CreateJS EventDispatcher

DESCRIPTION
Add Zim SelectionSet objects to a SelectionManager object and control multiple selection sets.
Used internally to control Squiggle and Blob point selections for multiple drags and mousemoves
Also provides key events along with undo event
See: https://zimjs.com/explore/selectionTest.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// manage two sets of SelectionSet objects:
var selectedCircles = new SelectionSet();
var selectedRects = new SelectionSet();
// add SelectionSet objects to manager and set ctrl to be multiple select
var selectionManager = new SelectionManager([selectedCircles, selectedRects], "ctrl");

var circles = new Tile(new Circle(), 5, 1, 50).center().cur();
var rects = new Tile(new Rectangle(), 5, 1, 50).center().mov(0,100).cur();
circles.on("click", function (e) {
	// toggle current selected - SelectionManager will handle multiple select
	selectedCircles.toggle(e.target);
	setColors();
});
rects.on("click", function (e) {
	selectedRects.toggle(e.target);
	setColors();
});
// need to loop through all objects to make sure their color did not change
function setColors() {
	circles.loop(function (circle) {
		circle.color = selectedCircles.isSelected(circle)?red:black;
	});
	rects.loop(function (rect) {
		rect.color = selectedRects.isSelected(rect)?red:black;
	});
	stage.update();
}
END EXAMPLE

PARAMETERS
sets - (default null) a ZIM SelectionSet object or an array of SelectionSet objects to manage
multipleKey - (default null) a key ("shift", "ctrl", "meta", etc.) to use for multiple select (only within a SelectionSet)
multipleSets - (default true) set to false to only allow one set at a time to be selected on

METHODS
clear() - clears all SelectionSet objects in the SelectionManager
setCurrent(set) - sets the privided SelectionSet to the currentSet and clears the others
dispose() - removes key events

PROPERTIES
type - holds the class name as a String
sets - an array of all the SelectionSet objects
multipleKey - the key provided for multiple select ("shift", "ctrl", etc) - do not add "Key" as in "shiftKey"
multiple - true if the multipleSelect key is being pressed otherwise false
ctrlKey - true if the ctrlKey key is being pressed otherwise false
shiftKey - true if the shiftKey key is being pressed otherwise false
metaKey - true if the metaKey key is being pressed otherwise false

EVENTS
dispatches an "undo" event if a CTRL or META plus the U key is pressed
--*///+81.6
	zim.SelectionManager = function(sets, multipleKey, multipleSets) {
		z_d("81.6");
		this.cjsEventDispatcher_constructor();
		this.type = "SelectionManager";
		if (zot(sets)) sets = [];
		if (zot(multipleSets)) multipleSets = true;
		if (!Array.isArray(sets)) sets = [sets];
		this.sets = sets;
		this.multipleKey = multipleKey;
		this.multipleSets = multipleSets;
		this.multiple = false;
		var that = this;
		for (var i=0; i<sets.length; i++) {
			sets[i].selectionManager = this;
		}
		this.clear = function() {
			this.currentSet = null;
			for (var i=0; i<sets.length; i++) {
				sets[i].clear();
			}
		}
		this.setCurrent = function(set) {
			this.currentSet = set;
			for (var i=0; i<sets.length; i++) {
				if (sets[i] == set) continue;
				if (!this.multipleSets) sets[i].clear();
			}
		}
		var eDown = new createjs.Event("keydown");
		this.eventRemove = eDown.remove;
		this.keydownEvent = window.addEventListener("keydown", function(e) {
			e.remove = that.eventRemove;
			if (that.multipleKey) that.multiple = e[that.multipleKey+"Key"];
			that.ctrlKey = e.ctrlKey;
			that.shiftKey = e.shiftKey;
			that.metaKey = e.metaKey;
			that.dispatchEvent(e);
			if (e.keyCode==90 && (that.ctrlKey || that.metaKey)) {
				that.dispatchEvent("undo");
			}
		});
		this.keyupEvent = window.addEventListener("keyup", function(e) {
			if (that.multipleKey) that.multiple = e[that.multipleKey+"Key"];
			that.ctrlKey = e.ctrlKey;
			that.shiftKey = e.shiftKey;
			that.metaKey = e.metaKey;
			e.remove = that.eventRemove;
			that.dispatchEvent(e);
		});
		this.dispose = function () {
			window.removeEventListener("keydown", this.keydownEvent);
			window.removeEventListener("keyup", this.keyupEvent);
		}
	}
	zim.extend(zim.SelectionManager, createjs.EventDispatcher, null, "cjsEventDispatcher", false);
	//-81.6

// SUBSECTION CONTROLLERS

/*--
zim.Swipe = function(obj, distance, duration, isometric)

Swipe
zim class - extends a createjs.EventDispatcher

DESCRIPTION
Sets up capturing swipes on objects.
Dispatches a "swipe" event on swipe left, right, up, down.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var rect = new Rectangle(200, 200, "blue");
rect.center(stage);
var swipe = Swipe(rect);
var distance = 100;
swipe.on("swipe", function(e) {
	zog(e.swipeX); // -1, 0, 1  (for left, none and right)
	zog(e.swipeY); // -1, 0, 1  (for up, none and down)

	// move directly:
	// rect.x += distance * e.swipeX;
	// rect.y += distance * e.swipeY;
	// stage.update();

	// or animate
	move({
		target:rect,
		x:rect.x+distance*e.swipeX,
		y:rect.y+distance*e.swipeY,
		time:400,
		ease:"quadOut"
	});
});
stage.update();
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
obj - the object you want to swipe on
distance - (default 30) the distance in pixels to activate swipe
	might want to pass in a pixel distance based on percentage of stage
duration - (default 80) time in milliseconds to travel that distance
	try https://zimjs.com/swipe.html for testing distance and time (speed)
isometric - (default false) set to true to rotate swiping test by 30 degrees
 	for swiping on ZIM game Board({isometric:true})
	left-right is from top left to bottom right (cols)
	top-bottom is from top right to bottom left (rows)

METHODS
enable() - set swipe to active (by default it is)
disable() - set swipe to inactive (sets active to false and does not dispatch)

PROPERTIES
type - holds the class name as a String
distance - the distance needed for swipe to activate
duration - the time from mousedown a swipe is measured for distance
direction - the direction of the last swipe (left, right, up, down or none)
obj - the object that was last swiped
active - Boolean true for dispatching swipes and false for not

EVENTS
dispatches a "swipe" event on every pressup (even if swipe failed and direction is none)
when a swipe event triggers
the Swipe event object has a swipeX and swipeY property that is -1,0, or 1
for left, none, or right OR up, none, down
the event object has an obj property as well for what object was swiped
also dispatches a "swipedown" event for convenience on a mousedown

LEGACY
the Swipe object provides a direction property of "left", "right", "up", or "down"
the Swipe object provides an obj property of what object was swiped on
for instance if e is the event object
then e.target is the Swipe object so use e.target.direction
did not dispatch a custom event due to lack of support in early IE
Swipe also dispatches a direction of "none" if the mouse movement is not a swipe
this can be used to snap back to an original location
--*///+70
	zim.Swipe = function(obj, distance, duration, isometric) {
		var sig = "obj, distance, duration, isometric";
		var duo; if (duo = zob(zim.Swipe, arguments, sig, this)) return duo;
		z_d("70");
		this.cjsEventDispatcher_constructor();
		this.type = "Swipe";

		if (zot(obj) || !obj.on) {zog("zim controls - Swipe():\nPlease pass in object"); return;}
		if (zot(distance)) distance = 30; // pixels for swipe to count
		if (zot(duration)) duration = 80; // ms to test pixels

		this.distance = distance;
		this.duration = duration;
		this.active = true;

		var startX;
		var startY;
		var mouseX;
		var mouseY;
		var downCheck;
		var timer;
		var that = this;

		var stage;
		obj.on("mousedown", function(e) {

			if (!that.active || e.target.zimNoSwipe) return;
			that.obj = e.target;
			stage = e.target.stage;
			mouseX = startX = e.stageX/stage.scaleX;
			mouseY = startY = e.stageY/stage.scaleY;
			downCheck = true;
			that.dispatchEvent("swipedown");
			clearTimeout(timer);
			timer = setTimeout(function() {
				if (downCheck) {
					checkSwipe();
					downCheck = false;
				}
			}, that.duration);
			obj.on("pressmove", function(e) {
				mouseX = e.stageX/stage.scaleX;
				mouseY = e.stageY/stage.scaleY;
			});
			obj.on("pressup", function(e) {
				if (downCheck) {
					checkSwipe();
					downCheck = false;
					clearTimeout(timer);
				}
			});

			function checkSwipe() {
				var swipeCheck = false;
				var ev = new createjs.Event("swipe");
				ev.obj = that.obj;
				ev.stageX = e.stageX/stage.scaleX;
				ev.stageY = e.stageY/stage.scaleY;
				ev.rawX = e.rawX/stage.scaleX;
				ev.rawY = e.rawY/stage.scaleY;
				ev.swipeX = ev.swipeY = 0;
				that.direction = "none";
				// may as well use 45 degrees rather than figure for aspect ratio

				if (isometric) {
					if (Math.abs(mouseX - startX) > that.distance || Math.abs(mouseY - startY) > that.distance) {
						// thought it was 30 degrees - turns out it is based on 90 degrees - so could calculate more easily...
						var angle = Math.atan2((mouseY - startY),(mouseX - startX))*180/Math.PI;
						angle = (angle+360)%360;
						if (angle>=270 && angle<360) {ev.swipeY = -1; that.direction="up";}
						else if (angle>=0 && angle<90) {ev.swipeX = 1;  that.direction="right";}
						else if (angle>=90 && angle<180) {ev.swipeY = 1;  that.direction="down";}
						else if (angle>=180 && angle<270) {ev.swipeX = -1; that.direction="left";}
					}
				} else {
					// 45 degree based
					if (Math.abs(mouseX - startX) > Math.abs(mouseY - startY)) {
						if (mouseX - startX > that.distance) {ev.swipeX = 1;  that.direction="right";}
						if (startX - mouseX > that.distance) {ev.swipeX = -1; that.direction="left";}
					} else {
						if (mouseY - startY > that.distance) {ev.swipeY = 1;  that.direction="down";}
						if (startY - mouseY > that.distance) {ev.swipeY = -1; that.direction="up";}
					}
				}
				that.dispatchEvent(ev);
			}
		});

		this.disable = function() {
			that.active = false;
		}

		this.enable = function() {
			that.active = true;
		}
	}
	zim.extend(zim.Swipe, createjs.EventDispatcher, null, "cjsEventDispatcher", false);
	//-70

/*--
zim.Swiper = function(swipeOn, target, property, sensitivity, horizontal, min, max, damp, integer, factor, pauseTime)

Swiper
zim class - extends a createjs EventDispatcher

DESCRIPTION
Swiper lets you change a property of any object (with damping) by swiping.
In a sense, it is like an invisible Slider.
You pass in the DisplayObject to swipe on - stage, Container, Bitmap, etc.
You pass in which object holds the property to animate and the property name.
Then Swiper will change this property with damping based on a sensitivity you set.
You can use horizontal or vertical but to do both, you need to make two Swiper objects.
Originally made for controlling 3D objects like rotation and scale
based on swiping a rectangle beneath the 3D object that is the same color as the stage.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle(100, green).center(stage);
// will move circle twice as fast as swipe
var swiper = new Swiper(stage, circle, "x", 2);

var man = new Rectangle(50, 100, brown).center(stage);
// will move man up an down slowly within vertical bounds of stage
var swiper = new Swiper(man, man, "y", .5, false, 0, stageH-man.height);
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
swipeOn - the DisplayObject to swipe on such as the stage or a Rectangle or Bitmap, etc.
target - the object that holds the property that you want to change
property - the property name as a String to change when swiping
sensitivity - (default 1) the change in property is equal to the change in distance times the sensitivity
	set to 2 to change the property twice as fast as the swipe
	set to .5 to change the property half as fast as the swipe
	set to .001 to change the property very little while swiping
	set to -1 to go the opposite way (or -2, -.5, -.001, etc.)
horizontal - default(true) set to false for vertical swiping (y)
min - (default null) if specified, the property value will not go below this number
max - (default null) if specified, the property value will not go above this number
damp - (default .1) the damp value with 1 being no damping and 0 being no movement
integer - (default false) set to true to round the property value
factor - (default 1) is going the same direction and -1 is going in opposite direction
pauseTime - (default 200ms) time in ms to call swipepause event if no swipe motion and swipeOn is pressed

METHODS
immediate(val) - set the damping immediately to this value to avoid damping to value
dispose() - remove listeners and Ticker

PROPERTIES
type - holds the class name as a String
target - get or set the target for the property that you are changing
property - get or set the String property name that is being damped
desiredValue - the current value that the swiper is damping towards
enabled (default true) - set to false to disable the Swiper and visa versa

EVENTS
dispatches a "swipedown" event when swipe is started
dispatches a "swipemove" event when swipe is moving
dispatches a "swipeup" event when swipe is ended
dispatches a "swipepause" event when finger is not swiping but still down - see pauseTime parameter
dispatches a "swipestop" event when swipeup has happened and value has stopped changing (delay is due to damp)
--*///+69.5
	zim.Swiper = function(swipeOn, target, property, sensitivity, horizontal, min, max, damp, integer, factor, pauseTime) {
		var sig = "swipeOn, target, property, sensitivity, horizontal, min, max, damp, integer, factor, pauseTime";
		var duo; if (duo = zob(zim.Swiper, arguments, sig, this)) return duo;
		z_d("69.5");

		this.cjsEventDispatcher_constructor();
		this.type = "Swiper";

		if (zot(swipeOn) || !swipeOn.getStage || !swipeOn.stage) {zog("zim.Swiper() - please provide container on stage"); return;}
		if (zot(target)) return;
		if (zot(sensitivity)) sensitivity = 1;
		if (zot(horizontal)) horizontal = true;
		if (zot(damp)) damp = .1;
		if (zot(integer)) integer = false;
		if (zot(factor)) factor = 1;
		if (zot(pauseTime)) pauseTime = 200;

		var that = this;
		var container = swipeOn;
		var startPos;
		var startVal;
		var desiredVal = that.desiredValue = target[property];
		var stage;
		this.target = target;
		this.property = property;
		var downEvent;
		var moveEvent;
		var upEvent;
		var downCheck = false;
		if (container.canvas) {
			downEvent = container.on("stagemousedown", function(e) {
				downCheck = true;
				downHandler(e);
				moveEvent = container.on("stagemousemove", pressHandler);
				upEvent = container.on("stagemouseup", function() {
					downCheck = false;
					container.off("stagemousemove", moveEvent);
					container.off("stagemouseup", upEvent);
					that.dispatchEvent("swipeup");
				});
			});
			stage = container;
		} else {
			stage = container.stage;
			offMouseEvents();
			downEvent = container.on("mousedown", downHandler);
			moveEvent = container.on("pressmove", pressHandler);
			upEvent = container.on("pressup", function() {
				downCheck = false;
				that.dispatchEvent("swipeup");
			});
		}
		var stage;
		function downHandler(e) {
			stage = e.target.stage;
			downCheck = true;
			startPos = horizontal?e.stageX/stage.scaleX:e.stageY/stage.scaleY;
			startVal = that.target[that.property];
			that.dispatchEvent("swipedown");
		}
		function pressHandler(e) {
			var diff = startPos-(horizontal?e.stageX/stage.scaleX:e.stageY/stage.scaleY);
			if (Math.abs(diff) > 0) that.swiperMoving = true;
			desiredVal = startVal - diff*sensitivity*factor;
			if (!zot(min)) desiredVal = Math.max(desiredVal, min);
			if (!zot(max)) desiredVal = Math.min(desiredVal, max);
			that.desiredValue = desiredVal;
			that.dispatchEvent("swipemove");
		};
		var swiperDamp = new zim.Damp(that.target[that.property], damp);
		var lastValue = that.target[that.property];
		var lastDesiredVal = 0;
		var lastPausedVal = null;
		that.target.swiperTicker = zim.Ticker.add(function() {
			if (desiredVal == lastDesiredVal) {
				if (desiredVal != lastPausedVal) {
					if (that.pauseTimeout) that.pauseTimeout.clear();
					lastPausedVal = desiredVal;
					that.pauseTimeout = zim.timeout(pauseTime, function() {
						that.dispatchEvent("swipepause");
						that.pauseTimeout = null;
					});
				}
			} else {
				lastPausedVal = null;
				lastDesiredVal = desiredVal;
				if (that.pauseTimeout) that.pauseTimeout.clear();
			}
			if (!that.swiperMoving) return;
			that.target[that.property] = integer?Math.round(swiperDamp.convert(desiredVal)):swiperDamp.convert(desiredVal);
			if (!downCheck && !zot(min) && !zot(max) && Math.abs(that.target[that.property]-desiredVal) < Math.abs(max-min)/1000) {
				that.swiperMoving = false;
				that.target[that.property] = desiredVal; // snap to final value
				that.immediate(that.target[that.property]);
				that.dispatchEvent("swipestop");
			} else {
				lastValue = that.target[that.property];
			}
		}, stage);

		this.immediate = function(val) {
			swiperDamp.immediate(val);
			that.target[that.property] = integer?Math.round(val):val;
			that.desiredValue = desiredVal = val;
		}

		var _enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return _enabled;
			},
			set: function(value) {
				if (_enabled == value) return;
				if (value) {
					enable();
				} else {
					disable();
				}
				_enabled = Boolean(value);
			}
		});

		Object.defineProperty(that, 'desiredValue', {
			get: function() {
				return desiredVal;
			},
			set: function(value) {
				desiredVal = value;
			}
		});

		function offStageEvents() {
			container.off("stagemousedown", downEvent);
			container.off("stagemousemove", moveEvent);
			container.off("stagemouseup", upEvent);
		}
		function offMouseEvents() {
			container.off("mousedown", downEvent);
			container.off("pressmove", moveEvent);
			container.off("pressup", upEvent);
		}

		function disable() {
			if (container.canvas) offStageEvents();
			else offMouseEvents();
			zim.Ticker.remove(that.target.swiperTicker);
		}

		function enable() {
			if (container.canvas) {
				downEvent = container.on("stagemousedown", downEvent);
			} else {
				downEvent = container.on("mousedown", downEvent);
				moveEvent = container.on("pressmove", moveEvent);
				upEvent = container.on("pressup", upEvent);
			}
			that.immediate(that.target[that.property]);
			that.target.swiperTicker = zim.Ticker.add(that.target.swiperTicker, stage);
		}

		this.dispose = function() {
			disable();
			swiperDamp = null;
		}
	}
	zim.extend(zim.Swiper, createjs.EventDispatcher, null, "cjsEventDispatcher", false);
	//-69.5

/*--
zim.MotionController = function(target, type, speed, axis, boundary, map, diagonal, damp, flip, rotate, constant, firstPerson, turnSpeed, moveThreshold, stickThreshold, container, localBounds, mouseMoveOutside, mousedownIncludes, minPercentSpeed, maxPercentSpeed, dampKeyup)

MotionController
zim class - extends a createjs EventDispatcher

DESCRIPTION
MotionController lets you control an object (target) in a container (like the stage)
with "mousedown", "mousemove", "keydown", "gamebutton", "gamestick" or "manual" modes (types)
For instance, you can control a player in a game or a butterfly in field
SEE: https://zimjs.com/controller for more examples
SEE: https://zimjs.com/explore/sidescroller.html for keyboard work with Scroller, Sprite, Dynamo, Accelerator
SEE: https://zimjs.com/pen or https://zimjs.com/genpen (complex example)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle(40, green).center(stage);
var controller = new MotionController(stage, circle); // circle moves to mouse press position with damping

var rect = new Rectangle(50, 30, green).centerReg(stage);
var controller = new MotionController({
	container:stage,
	target:rect,
	type:"keydown",
	diagonal:true,
	damp:.1,
	rotate:true
});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
target (default null) - the object you want to control
	example a circle to move across the stage
	can also be an Accelerator() to control percentSpeed
	if you only want data from the MotionController you can leave the target parameter as null (don't include it)
type - (default "mousedown") by default will move to where you press in the container
	set to "mousemove" to have the target follow the mouse movement
	set to "pressmove" to have target jump to mousedown position then follow while pressing
	set to "keydown" to use keys to control the target (see map parameter)
	set to "gamebutton" to use gamepad buttons to control the target (see map parameter)
	set to "gamestick" to use gamepad stick(s) to control the target (see map parameter)
	set to "swipe" to use swipe to control the target
	set to "manual" to set your own with myController.convert() or myController.x and myController.y properties
speed - (default 7) pixels it will move each tick, keypress buttonpress, swipe, etc.
axis - (default "both" or "horizontal" if target is Accelerator) or "horizontal" or "vertical" (see diagonal parameter)
boundary - (default null) a ZIM Boundary object for the boundary
	the registration point of the target will stay within this boundary
	this boundary is relative to the stage (global)
	if a boundary relative to the object's parent is desired then set the localBounds parameter to true
	ignored when target is Accelerator - use minPercentSpeed and maxPercentSpeed instead
map - (default null) an Array with left, right, up, down values (or array of values) as outlined below
 	- (default [[65,37], [68,39], [87,38], [83,40]] when type == "keydown") these are ADWS and arrows
	- (default [14, 15, 12, 13] when type == "gamebutton") these are DPAD_LEFT, DPAD_RIGHT, DPAD_UP, DPAD_DOWN on a gamepad
	- (default [14, 15, 7, 6] when type == "gamebutton" and firstPerson == true) these are DPAD_LEFT, DPAD_RIGHT, RT, LT on a gamepad
	- (default [0, 0, 1, 1] when type == "gamestick") these are LSX, LSX, LSY, LSY on a gamepad
	- (default [[0,2], [0,2], [1], [1]] when type == "gamestick" and firstPerson == true) turn with left or right stick X, advance with left stick Y
		use [[0,2], [0,2], [1,3], [1,3]] for both sticks (first stick motion overrides second stick)
		Note: MotionController will only use the 0 and the 2 index for speed as the sticks use -1 to 1 values
		so you could not go only left with one stick and only right with another
		Note: stick values may exceed -1 and 1 on occasion (see also stickThreshold)
diagonal - (default true) set to false to lock movement to horizontal or vertical only
damp - (default .1 or .2 if type is pressmove) the damp value with 1 being no damping and 0 being no movement
flip - (default null) set to "horizontal", "vertical" or "both" to flip the target when in negative direction
rotate - (default false) set to true to rotate - starts facing right and rotates in direction of movement
constant - (default false) set to true to remove keyup or gamebutton up and always move in direction last key or button press
firstPerson - (default false) set to true for keydown, gamebutton and gamecontroller to go to first person mode
	in firstPerson, the direction starts facing up and by default up arrow is speed forward and left and right change rotation
	speed will be damped by damp parameter - also, map parameter changes if in firstPerson mode - see map parameter
turnSpeed - (default speed*.4) - the speed for turning in firstPerson mode - will be damped but damp parameter
moveThreshold - (default 5) pixels negative or positive to treat damped motion as stopped
stickThreshold - (default .2) gamepad stick axes values are from -1 to 1 but there is a lot of noise
	so consider within +- stickThreshold as no motion 0
container - (default zimDefauktFrame stage) the Container the target is in - the stage is most likely fine
	if container is specified, it must be on the stage when the MotionController is made
localBounds - (default false) which means the boundary is global - set to true for a boundary in the object parent frame
mouseMoveOutside - (default true) set to false to not allow mouse movement outside the stage to affect motion
mousedownIncludes - (default null) a single object or array of objects (aside from the stage) to activate with mousedown
	For mousedown and pressmove types, the mousedown will activate on the stage only
	The controller assumes everything in the container will not activate the mousedown
	This lets you activate interface elements without moving to them ;-)
	If for instance, a backing is used other than the stage, just pass in the backing to this parameter
	See also the mousedownIncludes property and mousedownExcludes parameter and property
mousedownExcludes - (default null) an array of objects that the mousedown will not work on - overrides mousedownIncludes
minPercentSpeed - (default 0) if target is an Accelerator, the percentSpeed at the left or top of the stage (depending on axis)
minPercentSpeed - (default 100) if target is an Accelerator, the percentSpeed at the right or bottom of the stage (depending on axis)
dampKeyup - (default .3) damping applied to slow down Accelerator with keydown

METHODS
immediate(x, y) - set the damping immediately to this value to avoid damping to value - returns object for chaining
convert(x, y) - for manual mode, pass in x and y and damping and rotation will be calculated
dispose() - remove listeners and Ticker, Swiper and GamePad, where applicable

PROPERTIES
type - holds the class name as a String
target - the target that you are controlling
	target.x and target.y will give you damped x and y
x - the desired x position of the target before damping is applied (use this for manual input - or convert() method)
y - the desired y position of the target before damping is applied (use this for manual input - or convert() method)
dirX - the x direction the player is moving
dirY - the x direction the player is moving
rotation - read only rotation of the player in degrees
scaleX - read only scaleX of player (to get flip data if only using controller for data)
scaleY - read only scaleY of player (to get flip data if only using controller for data)
dampX - reference to the horizonal Damp object
dampY - reference to the vertical Damp object
dampKeyup - get or set keyup damping for when using an accelerator
speed - the speed setting which will be multiplied by direction
turnSpeed - the turn speed for firstPerson mode
axis - the axis (horizontal, vertical or both);
moving - get Boolean as to whether the target is moving (within moveThreshold)
movingX - get Boolean as to whether the target is moving in x direction (within moveThreshold)
movingY - get Boolean as to whether the target is moving in y direction (within moveThreshold)
boundary - get or set the Boundary object
gamepad - reference to GamePad object if applicable - allows you to use this for more events like jumping, shooting, etc.
moveThreshold - the maximum value (+-) within which movement is considered stopped
stickThreshold - the maximum value (+-) within which the gamepad stick axes values are considered 0
mousedownIncludes - an array of objects that the mousedown will work on - along with the stage
enabled - set to false to disable or true to enable MotionController - can toggle with enabled = !enabled

EVENTS
dispatches a "change" event for changes in direction with dir as property of event object
	that will hold "left", "right", "up", "down", null (no direction)
dispatches a "mousedown" event if type is "mousedown" or "pressmove"
dispatches a "pressing" event if type is "pressmove" - note, this dispatches even if not moving
dispatches a "moving" event if target is moving and "startmoving" and "stopmoving" events
--*///+69.7
	zim.MotionController = function(target, type, speed, axis, boundary, map, diagonal, damp, flip, rotate, constant, firstPerson, turnSpeed, moveThreshold, stickThreshold, container, localBounds, mouseMoveOutside, mousedownIncludes, minPercentSpeed, maxPercentSpeed, dampKeyup) {
		var sig = "target, type, speed, axis, boundary, map, diagonal, damp, flip, rotate, constant, firstPerson, turnSpeed, moveThreshold, stickThreshold, container, localBounds, mouseMoveOutside, mousedownIncludes, minPercentSpeed, maxPercentSpeed, dampKeyup";
		var duo; if (duo = zob(zim.MotionController, arguments, sig, this)) return duo;
		z_d("69.7");

		this.cjsEventDispatcher_constructor();
		this.type = "MotionController";

		var stage;
		var message = "zim MotionController(): Please pass in a reference to the stage or a container on the stage";
		if (zot(container)) {
			if (zimDefaultFrame) {
				stage = container = zimDefaultFrame.stage;
			} else {
				zog(message);
				return;
			}
		} else if (zot(container.stage)) {
			if (zimDefaultFrame) {
				stage = zimDefaultFrame.stage;
			} else {
				zog(message);
				return;
			}
		} else {
			stage = container.stage
		}

		if (zot(target)) {target = new zim.Container(1,1, null, null, false);} // make a surrogate if only wanting controller data
		var accelerator = target.type == "Accelerator";
		if (zot(speed)) speed = 7;
		if (zot(type) || (type != "mousemove" && type != "pressmove" && type != "keydown" && type != "gamebutton" && type != "gamestick" && type != "swipe" && type != "manual")) type = "mousedown";
		if (zot(axis)) axis = accelerator?"horizontal":"both"; // horizontal, vertical, both
		if (type == "keydown" && zot(map)) map = [[65,37], [68,39], [87,38], [83,40]] // left right up down
		if (type == "gamebutton" && zot(map)) {
			if (firstPerson) {
				map = [14, 15, zim.GamePad.RT, zim.GamePad.LT] // DPAD_LEFT, DPAD_RIGHT, RT, LT on gamepad
			} else {
				map = [14, 15, 12, 13] // DPAD_LEFT, DPAD_RIGHT, DPAD_UP, DPAD_DOWN on gamepad
			}
		}
		if (type == "gamestick" && zot(map)) {
			if (firstPerson) {
				map = [[0,2], [0,2], [1], [1]]; // TURN: LSX or RSX, LSX or RSX, SPEED: LSY, LSY - on gamepad
			} else {
				map = [0, 0, 1, 1]; // LSX, LSX, LSY, LSY - Left Stick on gamepad
			}
		}

		if (type == "gamestick" && zot(map)) map = [0, 0, 1, 1]; // LSX, LSX, LSY, LSY - Left Stick on gamepad
		if (zot(diagonal)) diagonal = true;
		if (axis == "horizontal" || axis == "vertical") diagonal = false;
		if (zot(damp)) damp = (type=="keydown" || type=="gamebutton") ? 1: (type=="pressmove"?.2:.1);
		if (zot(firstPerson)) firstPerson = false;
		if (zot(turnSpeed)) turnSpeed = speed * .4;
		if (zot(moveThreshold)) moveThreshold = 4;
		if (zot(stickThreshold)) stickThreshold = .2;
		if (zot(mouseMoveOutside)) mouseMoveOutside = false;
		stage.mouseMoveOutside = mouseMoveOutside;
		if (zot(mousedownIncludes)) mousedownIncludes = [];
		if (!Array.isArray(mousedownIncludes)) mousedownIncludes = [mousedownIncludes];
		if (target.type == "Pen" && !target.draggable) {
			mousedownIncludes.push(target, target.paper);
		} else {
			mousedownIncludes.push(target);
		}
		// if (zot(mousedownExcludes)) mousedownExcludes = [];
		// if (!Array.isArray(mousedownExcludes)) mousedownExcludes = [mousedownExcludes];
		this.mousedownIncludes = mousedownIncludes;
		// this.mousedownExcludes = mousedownExcludes;
		if (zot(dampKeyup)) dampKeyup = .3;

		var that = this;
		that.dampKeyup = dampKeyup;
		this.dirX = 0;
		this.dirY = 0;
		this.speed = speed;
		this.turnSpeed = turnSpeed;
		this.axis = axis;
		this.target = target;
		this.moveThreshold = moveThreshold;
		this.stickThreshold = stickThreshold;
		this.boundary = boundary;

		var speedX = that.speed; // speedX and speedY hold proportioned speed based on angle
		var speedY = that.speed;
		var flipRotation = 0; // records if we need to adjust rotation if flipped
		that.rotation = 0; // holds the pre-damped angle of the target

		var proportion;
		var bb;
		if (accelerator) {
			bb = container.getBounds();
			var dist;
			if (!bb || (axis == "vertical" && !bb.height) || (axis != "vertical" && !bb.width)) dist = 1000;
			else dist=axis=="vertical"?bb.height:bb.width;
			if (zot(minPercentSpeed)) minPercentSpeed = 0;
			if (zot(maxPercentSpeed)) maxPercentSpeed = 100;
			if (zot(this.target.percentSpeed)) this.target.percentSpeed = (maxPercentSpeed-minPercentSpeed)/2;
			that.x = dist/2; // holds the pre-damped x and y position of the target
			that.y = dist/2;
			proportion = new zim.Proportion(0,dist,minPercentSpeed, maxPercentSpeed);
		} else {
			that.x = this.target.x; // holds the pre-damped x and y position of the target
			that.y = this.target.y;
		}

		var originalScaleX = that.scaleX = target.scaleX;
		var originalScaleY = that.scaleY = target.scaleY;

		// INPUTS
		// set up collecting the desired x and y based on various inputs:
		// keydown, gamebutton
		// mousedown, mousemove
		// gamestick
		// swipe
		// otherwise the setting is manual and MotionController x and y can be provided through calculate(x,y) method
		var mouseEvent;
		var mouseEvent2;
		var mouseEvent3;
		var pressing = false;
		var moveCheck = false;

		var stage;

		if (type == "keydown" || type == "gamebutton") {

			// which keys or buttons handle left, right, up, down are provided by the map parameter
			// this can be either a number or an array of numbers
			// so normalize this to always hold an array
			for (var i=0; i<4; i++) {
				if (!Array.isArray(map[i])) map[i] = [map[i]];
			}
			var down = [0,0,0,0];
			var ord = []; // order the keys are pressed - so when we release, we can set to last currently pressed key
			var way = ["X","X","Y","Y"];
			var dir = [-1,1,-1,1];
			var names = ["left","right","up","down"];
			var rots = [-180,0,-90,90];
			var d = {dirX:0, dirY:0}; // local directions for key and button - this.dirX and this.dirY are used in Ticker

			if (type == "keydown") {
				var keydownEvent = frame.on("keydown", doDown);
			} else {
				var gamepad = that.gamepad = new zim.GamePad();
				var buttondownEvent = gamepad.on("buttondown", doDown);
			}
			function doDown(e) {
				var key = type=="keydown"?e.keyCode:e.buttonCode;
				var inOrd;
				for (i=0; i<4; i++) {
					if (map[i].indexOf(key) > -1) {
						if (!diagonal && that.axis=="both") d.dirX = d.dirY = 0;
						d["dir"+way[i]] = dir[i];
						down[i] = 1;
						inOrd = ord.indexOf(i);
						if (inOrd == 0) return; // already last pressed
						if (inOrd > 0) ord.splice(inOrd,1); // take key out if already down
						ord.unshift(i); // add index to start of ord array
						return;
					}
				}
			}
			if (zot(constant)) {
				if (type == "keydown") {
					var keyupEvent = frame.on("keyup", doUp);
				} else {
					var buttonupEvent = gamepad.on("buttonup", doUp);
				}
			}
			function doUp(e) {
				var key = type=="keydown"?e.keyCode:e.buttonCode;
				var inOrd;
				for (i=0; i<4; i++) {
					if (map[i].indexOf(key) > -1) {
						down[i] = 0;
						inOrd = ord.indexOf(i);
						if (inOrd >= 0) ord.splice(inOrd,1);
						if (that.axis != "both" || diagonal) { // either just one direction or can have both dirX and dirY
							d["dir"+way[i]] = -down[Math.floor(i/2)*2] +down[Math.floor(i/2)*2+1]; // the other might be down
						} else { // only use last pressed key for dirX or dirY but not both
							if (ord.length > 0) {
								d["dir"+way[i]] = 0;
								var iOrd = ord[0];
								d["dir"+way[iOrd]] = dir[iOrd];
							} else {
								d.dirX = d.dirY = 0;
							}
						}
						return;
					}
				}
			}
			// use a ticker to position the desired x and y properties
			// we will then tween to these properties in the mainTicker later
			var first = {rotation:0, speedX:that.speed, speedY:that.speed};
			var keyTicker = zim.Ticker.add(function() {
				if (firstPerson) {doFirstPerson(d); return;}
				var sX = that.speed;
				var sY = that.speed;
				if (that.axis == "both" && d.dirX != 0 && d.dirY != 0) {
					var trig = doTrig(d.dirX, d.dirY); // note - keys need to place desired x and y in a unit manner (or else target never goes anywhere)
					sX = trig.speedX;
					sY = trig.speedY;
				}
				if (that.axis == "horizontal" || that.axis == "both") {
					that.x += sX * d.dirX;
				}
				if (that.axis == "vertical" || that.axis == "both") {
					that.y += sY * d.dirY;
				}
				calculate();
			}, stage);
		} else if (type == "mousedown" || type == "mousemove") {
			mouseEvent = stage.on("stage" + type, function(e){
				stage = e.target.stage;
				if (type == "mousedown") {
					if (!Array.isArray(that.mousedownIncludes)) that.mousedownIncludes = [that.mousedownIncludes];
					if (that.mousedownIncludes.indexOf(container)<0) {
						for (var i=0; i<container.numChildren; i++) {
							var child = container.getChildAt(i);
							if (that.mousedownIncludes.indexOf(child)==-1 && child.mouseEnabled && child.hitTestPoint && child.hitTestPoint(e.stageX/stage.scaleX, e.stageY/stage.scaleY)) return;
						}
					}
					that.dispatchEvent("mousedown");
				}
				var p = container.globalToLocal(mouseMoveOutside?e.rawX/stage.scaleX:e.stageX/stage.scaleX, mouseMoveOutside?e.rawY/stage.scaleY:e.stageY/stage.scaleY);
				that.x = p.x; that.y = p.y;
				calculate();
			});
		} else if (type == "pressmove") {
			if (target.type == "Pen") target.write = false;
			mouseEvent = stage.on("stagemousedown", function(e) {
				moveCheck = false;
				if (!Array.isArray(that.mousedownIncludes)) that.mousedownIncludes = [that.mousedownIncludes];
				var inCheck = false;
				var under = stage.getObjectUnderPoint(e.stageX, e.stageY);
				for (var i=0; i<that.mousedownIncludes.length; i++) {
					if (that.mousedownIncludes[i].hitTestPoint && that.mousedownIncludes[i].hitTestPoint(e.stageX, e.stageY) && that.mousedownIncludes[i].contains(under)) {
						inCheck = true;
					}
				}
				if (!inCheck) {
					for (var i=0; i<container.numChildren; i++) {
						var child = container.getChildAt(i);
						if (that.mousedownIncludes.indexOf(child)==-1 && child.mouseEnabled && child.hitTestPoint && child.hitTestPoint(e.stageX/stage.scaleX, e.stageY/stage.scaleY)) return;
					}
				}
				var p = container.globalToLocal(mouseMoveOutside?e.rawX/stage.scaleX:e.stageX/stage.scaleX, mouseMoveOutside?e.rawY/stage.scaleY:e.stageY/stage.scaleY);
				that.immediate(p.x, p.y);
				calculate();
				that.dispatchEvent("mousedown");
				pressing = true;
				mouseEvent2 = stage.on("stagemousemove", function(e){
					var p = container.globalToLocal(mouseMoveOutside?e.rawX/stage.scaleX:e.stageX/stage.scaleX, mouseMoveOutside?e.rawY/stage.scaleY:e.stageY/stage.scaleY);
					that.x = p.x; that.y = p.y;
					calculate();
					if (target.type == "Pen" && !moveCheck && target.drawing) moveCheck = true;
				});
				if (target.type == "Pen") {
					target.write = true;
					target.zimDragCheck = true;
				}
			});
			mouseEvent3 = stage.on("stagemouseup", function(e){
				if (target.type == "Pen") {
					target.write = false;
					target.zimDragCheck = false;
					if (moveCheck) target.stopCheck();
				}
				stage.off("stagemousemove", mouseEvent2);
				pressing = false;
			});

		} else if (type == "gamestick") {
			var gamepad = this.gamepad = new zim.GamePad();
			for (var i=0; i<4; i++) { // make map hold arrays
				if (!Array.isArray(map[i])) map[i] = [map[i]];
			}
			var first = {rotation:0, speedX:that.speed, speedY:that.speed};
			var stickEvent = gamepad.on("data", function(e) {

				d = {dirX:0, dirY:0};
				// map = [[0,2], [0,2], [1,3], [1,3]]
				for (var i=0; i<map[0].length; i++) {
					var a = e.axes[map[0][i]];
					if (Math.abs(a) > that.stickThreshold) {
						d.dirX = a;
						break;
					}
				}
				for (var i=0; i<map[2].length; i++) {
					var a = e.axes[map[2][i]];
					if (Math.abs(a) > that.stickThreshold) {
						d.dirY = a;
						break;
					}
				}

				if (firstPerson) {doFirstPerson(d); return;}

				that.x += that.speed*d.dirX;
				that.y += that.speed*d.dirY;
				calculate();
			});
		} else if (type == "swipe") {
			var swiperX = new zim.Swiper(stage, that, "x", .8);
			var swiperY = new zim.Swiper(stage, that, "y", .8, false);
			var swiperEvent = swiperX.on("swipemove", function() {
				calculate();
			});
		}

		function doFirstPerson(d) {
			first.rotation += d.dirX * that.turnSpeed;
			that.rotation = first.rotation;
			first.speedX = Math.sin(first.rotation*Math.PI/180) * that.speed * -d.dirY;
			first.speedY = - Math.cos(first.rotation*Math.PI/180) * that.speed * -d.dirY;
			that.x += first.speedX;
			that.y += first.speedY;
			return;
		}

		// CALCULATE
		// each input calls calculate to determine the angle of direction
		// and the speed along each axis, speedX and speedY
		function calculate() {

			// trig() returns an object with speedX, speedY and rotation properties
			var diffX = that.x-(accelerator?that.target.percentSpeed:that.target.x);
			var diffY = that.y-(accelerator?that.target.percentSpeed:that.target.y);
			var trig = doTrig(diffX, diffY);

			speedX = trig.speedX;
			speedY = trig.speedY;

			if (!rotate) return;
			that.rotation = trig.angle;
			if (zot(that.rotation)) {
				that.rotation = that.target.rotation;
				return; // when no motion purposely left null so stopped target keeps rotation
			}

			that.rotation += flipRotation; // if flipped we need to add 180 to rotation

			// make sure angle damps to shortest direction - this is tricky
			var newR = normalizeAngle(that.rotation);
			var oldR = that.target.rotation = normalizeAngle(that.target.rotation);
			if (Math.abs(newR-oldR) > 180) {
				if (oldR > newR) {
					oldR -= 360; // put current rotation behind new rotation so damps clockwise
				} else {
					newR -= 360; // put new rotation behind current rotation so damps counterclockwise
				}
			}
			that.dampR.immediate(oldR); // required otherwise damping equation has mind of its own
			that.target.rotation = oldR; // make sure to set this again as we may have changed oldR for proper rotational direction when damped
			that.rotation = newR;
		}
		function normalizeAngle(a) {
			return (a % 360 + 360) % 360;
		}
		function doTrig(diffX, diffY) {
			var sX = that.speed;
			var sY = that.speed;
			var sA; // keep angle null if no movement - so that last rotation during movement is kept when movement stops
			var hyp = Math.sqrt(Math.pow(diffX, 2) + Math.pow(diffY, 2));
			if (hyp > 0) {
				sX = Math.abs(diffX) / hyp * that.speed;
				sY = Math.abs(diffY) / hyp * that.speed;
				sA = 90 - Math.atan2(diffX, diffY)*180/Math.PI;
			}
			return {speedX:sX, speedY:sY, angle:sA};
		}


		// TICKER FOR ALL
		// damp movement and rotation to desired x and y

		var tempX = this.x = accelerator?dist/2:this.target.x;
		var tempY = this.y = accelerator?dist/2:this.target.y;

		this.dampX = new zim.Damp(tempX, damp);
		this.dampY = new zim.Damp(tempY, damp);
		this.dampR = new zim.Damp(this.target.rotation, damp);
		var lastDirX=0;
		var lastDirY=0;

		var mainTicker = zim.Ticker.add(function() {
			if (type == "manual") calculate();
			if (that.boundary) {
				that.x = zim.constrain(that.x, that.boundary.x, that.boundary.x+that.boundary.width);
				that.y = zim.constrain(that.y, that.boundary.y, that.boundary.y+that.boundary.height);
			}
			// tempX and tempY head towards the desired x and y
			// at the speed set by calculate and in the direction it needs to go calculated here
			// we then damp the actual motion using the dampX or dampY zim.Damp object's convert method
			if (that.axis == "horizontal" || that.axis == "both") {
				that.dirX = zim.sign(that.x-tempX);
				if (Math.abs(that.x-tempX) < speedX) {
					tempX = that.x;
				} else {
					tempX += that.dirX*speedX;
				}
				if (d && d.dirX==0) {
					var desired = that.boundary?that.boundary.width/2:stage.width/2;
					var newTempX = that.x-(that.x-desired)*(accelerator?that.dampKeyup:0);
					tempX = newTempX
					that.x = tempX;
				}
				if (accelerator && bb && bb.width) that.target.percentSpeed = proportion.convert((that.dampX.convert(tempX)));
				else that.target.x = that.dampX.convert(tempX);

			}
			if (that.axis == "vertical" || that.axis == "both") {
				that.dirY = zim.sign(that.y-tempY);
				if (Math.abs(that.y-tempY) < speedY) {
					tempY = that.y;
				} else {
					tempY += zim.sign(that.y-tempY)*speedY;
				}
				if (d && d.dirY==0) {
					var desired = that.boundary?that.boundary.height/2:stage.height/2;
					var newTempY = that.y-(that.y-desired)*(accelerator?that.dampKeyup:0);
					tempY = newTempY
					that.y = tempY;
				}
				if (accelerator && bb && bb.height) that.target.percentSpeed = proportion.convert(that.dampY.convert(tempY));
				else that.target.y = that.dampY.convert(tempY);
			}
			if (type == "pressmove" && pressing) that.dispatchEvent("pressing");

			// set moving properties
			that.movingX = Math.abs(tempX-(accelerator?that.target.percentSpeed:that.target.x)) > that.moveThreshold;
			that.movingY = Math.abs(tempY-(accelerator?that.target.percentSpeed:that.target.y)) > that.moveThreshold;
			if (that.moving && !(that.movingX || that.movingY)) {
				that.dispatchEvent("stopmoving");
			} else if (!that.moving && (that.movingX || that.movingY)) {
				that.dispatchEvent("startmoving");
			}
			that.moving = that.movingX || that.movingY
			if (that.moving) that.dispatchEvent("moving");

			// check for last direction change and dispatch event and flip if necessary
			if (that.dirX != lastDirX || that.dirY != lastDirY) {
				var e = new createjs.Event("change");
				if (that.dirX != lastDirX) {
					var options = ["left", null, "right"];
					e.dir = options[that.dirX+1];
					lastDirX = that.dirX;
					if (flip == "horizontal" || flip == "both") {
						that.scaleX = target.scaleX = that.dirX?Math.abs(target.scaleX)*that.dirX:target.scaleX;
						if (originalScaleX != 0 && Math.round(that.scaleX/originalScaleX) == -1) {
							flipRotation = 180;
						} else {
							flipRotation = 0;
						}
					}
				} else {
					var options = ["up", null, "down"];
					e.dir = options[that.dirY+1];
					lastDirY = that.dirY;
					if (flip == "vertical" || flip == "both") {
						that.scaleY = target.scaleY = that.dirY?Math.abs(target.scaleY) * -that.dirY:target.scaleY;
						// not sure why this breaks flip?
						// if (originalScaleY != 0 && Math.round(that.scaleY/originalScaleY) == -1) {
						// 	flipRotation = 180;
						// } else {
						// 	flipRotation = 0;
						// }
					}
				}
				that.dispatchEvent(e);
			}

			// damp the rotation - but not if the angle is null which happens when no movement
			// this keeps the last angle during movement rather than setting it to 0 which is not right
			if (rotate && !zot(that.rotation)) {
				that.target.rotation = that.dampR.convert(that.rotation);
			}

		}, stage);


		this.immediate = function(x,y,r) {
			if (!zot(x) && that.dampX) {
				that.dampX.immediate(x);
				if (accelerator) that.x = that.target.percentSpeed = tempX = x;
				else that.x = that.target.x = tempX = x;
				if (swiperX) swiperX.immediate(x);
			}
			if (!zot(y) && that.dampY) {
				that.dampY.immediate(y);
				if (accelerator) that.y = that.target.percentSpeed = tempY = y;
				else that.y = that.target.y = tempY = y;
				if (swiperY) swiperY.immediate(y);
			}
			if (!zot(r) && that.dampR) {
				that.dampR.immediate(r);
				that.rotation = that.target.rotation = r;
			}
			return that;
		}

		this.convert = function(x,y) {
			if (!zot(x)) that.x = x;
			if (!zot(y)) that.y = y;
			calculate();
		}

		var _enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return _enabled;
			},
			set: function(value) {
				if (_enabled == value) return;
				if (value) {
					enable();
				} else {
					disable();
				}
				_enabled = Boolean(value);
			}
		});
		function enable() {
			if (type == "keydown") {
				keydownEvent = frame.on("keydown", keydownEvent);
				keyupEvent = frame.on("keyup", keyupEvent);
				keyTicker = zim.Ticker.add(keyTicker, stage);
			} else if (type == "gamebutton") {
				buttondownEvent = gamepad.on("buttondown", buttondownEvent);
				buttonupEvent = gamepad.on("buttonup", buttonupEvent);
				keyTicker = zim.Ticker.add(keyTicker, stage);
			} else if (type == "gamestick") {
				stickEvent = gamepad.on("data", stickEvent);
			} else if (type == "swipe") {
				swiperX.enabled = true;
				swiperY.enabled = true;
				swiperEvent = swiperX.on("swipemove", swiperEvent);
			} else if (type == "mousedown" || type == "mousemove") {
				mouseEvent = stage.on("stage" + type, mouseEvent);
			} else if (type == "pressmove") {
				mouseEvent = stage.on("stagemousedown", mouseEvent);
				mouseEvent2 = stage.on("stagemousemove", mouseEvent2);
				mouseEvent3 = stage.on("stagemouseup", mouseEvent3);
			}
			mainTicker = zim.Ticker.add(mainTicker, stage);
		}
		function disable() {
			if (type == "keydown") {
				frame.off("keydown", keydownEvent);
				frame.off("keyup", keyupEvent);
				zim.Ticker.remove(keyTicker);
			} else if (type == "gamebutton") {
				gamepad.off("buttondown", buttondownEvent);
				gamepad.off("buttonup", buttonupEvent);
				zim.Ticker.remove(keyTicker);
			} else if (type == "gamestick") {
				gamepad.off("data", stickEvent);
			} else if (type == "swipe") {
				swiperX.enabled = false;
				swiperY.enabled = false;
				swiperX.off("swipemove", swiperEvent);
			} else if (type == "mousedown" || type == "mousemove") {
				stage.off("stage" + type, mouseEvent);
			} else if (type == "pressmove") {
				stage.off("stagemousedown", mouseEvent);
				stage.off("stagemousemove", mouseEvent2);
				stage.off("stagemouseup", mouseEvent3);
			}
			zim.Ticker.remove(mainTicker);

		}
		this.dispose = function() {
			disable();
			if (gamepad) gamepad.dispose();
			if (swiperX) swiperX.dispose();
			if (swiperY) swiperX.dispose();
		}

	}
	zim.extend(zim.MotionController, createjs.EventDispatcher, "enabled", "cjsEventDispatcher");
	//-69.7

/*--
zim.GamePad = function()

GamePad
zim class - extends a createjs EventDispatcher

DESCRIPTION
GamePad connects to Game Controllers as inputs using the HTML navigator.getGamepads API
Dispatches buttondown and buttonup events for the following common buttons:

"A","B","X","Y", (or for Triangle, Circle, Cross and Square)
"LB","RB","LT","RT", (for left bumper, right bumper, left trigger, right trigger)
"BACK","START",
"LS","RS", (for left stick press, right stick press)
"DPAD_UP","DPAD_DOWN","DPAD_LEFT","DPAD_RIGHT"

The event object will have a button property telling which button is pressed using the string values above
Dispatches a "data" event constantly to get axes data for the sticks (and constant data for the buttons)
The event object in this case will have axes and buttons properties
The axes property is an array of four numbers for the left and right stick's x and y properies (-1 to 1)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var gamepad = new GamePad();
gamepad.on("buttondown", function(e) {
	// only fires once per button press (unlike constant keydown event)
	zog(e.button); // LT for instance for Left trigger
	if (e.button == "LT") {
		zog("left trigger is down");
	}
	zog(e.buttonCode); // 6
	if (e.buttonCode == GamePad.LT) {
		zog("another way to do catch left trigger down");
	}
});

gamepad.on("buttonup", function(e) {
	zog(e.button); // LT for instance for Left trigger
}

gamepad.on("data", function(e) {
	// fires constantly in a requestAnimationFrame
	zog(e.axes[0]); // left stick x or horizontal data from -1 to 1 (lots of decimal noise)
	zog(e.axes[GamePad.LTX]); // another way of accessing left stick x
	zog(e.buttons[9]); // true or false depending on if the START button is pressed
	zog(e.buttons[GamePad.START]); another way to find if the START button is pressed
});
END EXAMPLE

METHODS
dispose() - removes all listeners and cancels requestAnimationFrame

PROPERTIES
type - holds the class name as a String
connected - Boolean true if connected and false if not connected (may need to press key, etc)
currentIndex - get or set the index of the controller
	gives multiple controller support - make two GameController objects and set different indexes
data - object that holds buttons (raw data - slightly different than buttons below) and axes properties
buttons - an array of Booleans as to whether the button is pressed
	the order of the buttons match the order of the constants below
constants: A,B,X,Y,LB,RB,LT,RT,BACK,START,LS,RS,DPAD_UP,DPAD_DOWN,DPAD_LEFT,DPAD_RIGHT
	GamePad.A == 0
	GamePad.B == 1, etc. up to
	GamePad.DPAD_RIGHT == 15
axes - an array of four stick values from -1 to 1
	for left x and y and right x and y values (or horizontal and vertical values)
constants: LSX,LSY,RSX,RSY
	GamePad.LSX == 0
	GamePad.LSY == 1
	GamePad.RSX == 2
	GamePad.RSY == 3

EVENTS
dispatches a "gamepadconnected" and gamepaddisconnected when connected and disconnected
	these have an event object with index and id properties - the index and id may not work in chrome
dispatches a "buttondown" event with button and buttonCode properties
dispatches a "buttonup" event with button and buttonCode properties
dispatches a "data" event with axes and buttons array properties
	these can be handled as outlined in the description and examples
--*///+69.8
	zim.GamePad = function() {
		z_d("69.8");
		this.type = "GamePad";

		this.cjsEventDispatcher_constructor();
		if (!navigator.getGamepads) {this.error = true; if (zon) {zog("zim.GamePad() - no browswer support");} return;} // if no gamepad support
		var processPad;
		window.addEventListener("gamepadconnected", init);
		this.currentIndex = 0;
		var that = this;
		function init(eventObject) {
			that.connected = true;
			dispatch("gamepadconnected", eventObject);
			var startData = navigator.getGamepads()[that.currentIndex];
			that.lastData = [];
			for (var i=0; i<startData.buttons.length; i++) {
				that.lastData[i] = startData.buttons[i].pressed;
			}
			function doPad() {
				processPad = requestAnimationFrame(doPad);
				that.data = navigator.getGamepads()[that.currentIndex];
				if (!that.data) return;
				var pressed = false;
				var currentData = that.buttons = [];
				for (var i=0; i<that.data.buttons.length; i++) {
					currentData[i] = that.data.buttons[i].pressed;
					if (currentData[i] != that.lastData[i]) {
						that.lastData[i] = currentData[i];
						if (currentData[i]) {
							// button was up and now is down
							// chose to dispatch only once unlike a keydown
							// if we want constant data then use data event and e.buttons
							var e = new createjs.Event("buttondown");
						} else {
							var e = new createjs.Event("buttonup");
						}
						e.buttonCode = i;
						e.button = gamePadButtons[i];
						that.dispatchEvent(e);
					}
				}
				var e = new createjs.Event("data");
				e.axes = that.axes = that.data.axes;
				e.buttons = that.buttons;
				that.dispatchEvent(e);
			}
			doPad();
		}
		var gamepadCheck = setInterval(function() { // for chrome
			if (navigator.getGamepads && navigator.getGamepads()[0]) {
				if (!that.connected) init();
				clearInterval(gamepadCheck);
			}
		}, 500);
		function dispatch(type, eventObject) {
			var e = new createjs.Event(type);
			e.index = eventObject.gamepad.index;
			e.id = eventObject.gamepad.id;
			e.buttons = eventObject.gamepad.buttons;
			e.axes = eventObject.gamepad.axes;
			that.dispatchEvent(e);
		}
		var disconnectEvent = window.addEventListener("gamepaddisconnected", function(e) {
			if (e.gamepad.index == that.currentIndex) {
				cancelAnimationFrame(processPad);
				connected = false;
				that.dispatchEvent("gamepaddisconnected");
			}
		});
		this.dispose = function() {
			window.removeEventListener("gamepadconnected", init);
			window.addEventListener("gamepaddisconnected", disconnectEvent);
			cancelAnimationFrame(processPad);
			clearInterval(gamepadCheck);
			that.connected = false;
		}
	}
	var gamePadButtons = ["A","B","X","Y","LB","RB","LT","RT","BACK","START","LS","RS","DPAD_UP","DPAD_DOWN","DPAD_LEFT","DPAD_RIGHT"];
	for (var i=0; i<gamePadButtons.length; i++) zim.GamePad[gamePadButtons[i]] = i;
	var gamePadAxes = ["LSX","LSY","RSX","RSY"];
	for (i=0; i<gamePadAxes.length; i++) zim.GamePad[gamePadAxes[i]] = i;
	zim.extend(zim.GamePad, createjs.EventDispatcher, null, "cjsEventDispatcher");
	//-69.8

/*--
zim.Portal = function(obj, lands)

Portal
zim class - extends a CreateJS EventDispatcher
Dedicated to Stephen Hawkings - may he be portal on!

DESCRIPTION
Turn an object into a portal that lets the user enter the portal to change lands, etc.
The portal works based on mouseover (or press for mobile)
The lands need to be stacked in a Container with the first land at the top.
Portal will pass users throught the lands and loop at the end back to the first land.
Alternatively, if loop is turned to false, Portal will backtrack the user through the lands
Pass the container of lands into the lands parameter - most often you will have two lands, but more are fine too!
Alternatively, the portal can be used without lands - and you can customize what you want to happen with portal events.

The object will be used as a mask to show the next land.
You can set the alpha of the object to any value above .01 to hide the object and show the land (do not use 0)
If your object is a ZIM shape, you can use rgba(0,0,0,.01) as the color and still have an opaque borderColor

SEE: https://zimjs.com/portal/

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
frame.loadAssets(["researchbuilding.jpg", "jungle.jpg"]);
frame.on("complete", function() {
	var lands = new Container(stageW, stageH).addTo(stage);
	var jungle = frame.asset("jungle.jpg")
		.scaleTo({boundObj:lands, type:"biggest"})
		.center(lands);
	var researchBuilding = frame.asset("researchbuilding.jpg")
		.scaleTo({boundObj:lands, type:"biggest"})
		.center(lands);
	var portalObject = new Circle(118, faint, pink, 16, true)
		.addTo(stage)
		.pos(580, 470)
		.animate({obj:{rotation:"360"}, time:70000, ease:"linear", loop:true});
	var portal = new Portal(portalObject, lands);
	portal.on("enter", function() {
		// play a sound here!
	});

	// use enabled to turn on and off portal
	timeout(1000, function() {portal.enabled = false; portalObject.pauseAnimate(true);});
	timeout(5000, function() {portal.enabled = true; portalObject.pauseAnimate(false);});

	stage.update();
}); // assets loaded
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
obj - the Display Object that will be the portal
lands - (default null) optional container of "lands" or display objects to portal through
	The Display Objects in the lands container (lands) should be on the stage
	The lands are adjusted by Portal so the currentLand is the second child and the nextLand is the first child.
	This is due to how the masking works.
	Inserting new lands or removing lands below an index of lands.numChildren-2 is okay
	Use addTo(lands, index) or removeFrom(lands).
	If you adjust either of the top two lands in the container, this will affect what is see on stage.

METHODS
dispose() - remove the events - the obj and lands will be left as is - manually remove these if needed

PROPERTIES
type - holds the class name as a String
portal - a reference to the portal obj
enabled - Boolean as to whether the portal is active or not
currentLand - get or set the active land if lands is provided
nextLand - get (read only) the next land to go to if lands is provided

EVENTS
dispatches an enter event on mouseover of the portal and an exit event on mouseout
--*///+69.96
	zim.Portal = function(obj, lands) {
		var sig = "obj, lands";
		var duo; if (duo = zob(zim.Portal, arguments, sig, this)) return duo;
		z_d("69.96");

		if (zot(obj) || !obj.stage) return;
		this.type = "Portal";

		var that = this;

		var landCheck = (lands && lands.numChildren && lands.numChildren > 0);
		if (landCheck) {
			var currentLand = lands.getChildAt(lands.numChildren-1);
			var nextLand = lands.getChildAt(lands.numChildren-2);
			lands.setChildIndex(nextLand, lands.numChildren-1);
			nextLand.setMask(obj);
		}
		function setPortal() {
			lands.setChildIndex(currentLand, 0); // send to back
			currentLand = nextLand;
			nextLand = lands.getChildAt(lands.numChildren-2);
			lands.setChildIndex(nextLand, lands.numChildren-1);
			currentLand.setMask(null);
			nextLand.setMask(obj);
			obj.stage.update();
		}

		obj.on(mobile()?"mousedown":"mouseover", function() {
			if (!that._enabled) return;
			if (landCheck) setPortal();
			that.dispatchEvent("enter");
		});

		obj.on(mobile()?"pressup":"mouseout", function() {
			if (!that._enabled) return;
			that.dispatchEvent("exit");
		});

		Object.defineProperty(this, 'portal', {
			get: function() {
				return obj;
			},
			set: function(s) {
				if (zot(s) || !s.stage){
					if (zon) {zog("zim.Portal() - please provide a Display Object to act as the portal"); return;}
				}
				nextLand.setMask(obj);
			}
		});

		Object.defineProperty(this, 'currentLand', {
			get: function() {
				return currentLand;
			},
			set: function(land) {
				if (!lands.contains(land)) return;
				while(land != currentLand) {
					setPortal();
				}
			}
		});

		Object.defineProperty(this, 'nextLand', {
			get: function() {
				return nextLand;
			},
			set: function(land) {
				if (zon) {zog("zim.Portal() - nextLand is read only - remake Portal to change"); return;}
			}
		});

		this._enabled = true;
		Object.defineProperty(that, 'enabled', {
			get: function() {
				return that._enabled;
			},
			set: function(value) {
				that._enabled = value;
			}
		});

		this.dispose = function() {

		}
	}
	zim.extend(zim.Portal, createjs.EventDispatcher, null, "cjsEventDispatcher", false);
	//-69.96

//
/*--
zim.Physics = function(gravity, borders, scroll, frame)

Physics
zim module - must import ZIM physics JavaScript file
SEE: https://zimjs.com/physics/

DESCRIPTION
The Physics class provides a wrapper for the Box2D physics engine.
Both Box2D and the ZIM physics JavaScript files must be imported after ZIM.
Once installed, physics can be turned on for ZIM DisplayObjects such as:
Rectangle, Circle, Triangle, Bitmap, Label, Sprite, Shape, etc.

NOTE: The DisplayObjects used in Physics should have center registration and be on the stage
or in a non-transformed Container at 0,0 on the stage.

Use the DisplayObject's addPhysics() method to add the object to a physics world.
This will also add more methods and properties to the DisplayObject.
The physics world can be set up ahead of time with new Physics()
or if a world is not already created the addPhysics() method will make a default world.
The default world will have a boundary of the frame and gravity of 10 (normal gravity).

SHAPES
By default the physics body shape will be a rectangle that matches the bounding box of the DisplayObject.
However, a Circle will be have a physics circle and a Triangle will have a physics triangle.
A different shape can be passed in to the physics() method if desired.
The expand parameter can be used to make the physics object bigger or smaller than the bounds.

PHYSICS
Objects have impulse(), force(), spin() and torque() methods to push them around and spin them.
impulse() is a one time push like shooting a pool ball
force() is a force over time like gravity or wind and is applied in a Ticker, keydown, etc.
spin() is a one time spin of the object around its center
torque() will rotate the object over time and is applied in a Ticker, keydown, etc.
The physics class has a join() method that can join objects in a variety of physics joints.
The physics class has a drag() method to specify which objects are draggable.
There is a debug() method to see the physics world behind ZIM.
The world can be set up to be bigger than the frame and follow() an object.
Objects are given a control() method to use keyboard keys to move the object.
Contact can be tested for with contact() and contactEnd()
each which receive a callback function that is provided with the other contacting body

BOX2D
http://www.box2dflash.org/docs/2.1a/reference/
http://www.box2dflash.org/docs/2.0.2/manual
Box2D has many other features that can be accessed as well.
Note that the ZIM Physics makes much of this easier
so start with ZIM and use the Box2D docs and manuals for custom requirements.
The Physics class has a world poperty that refers to the Box2D world.
Each ZIM DisplayObject with physics() turned on has a body property
that refers to the Box2D body and a physics property that refers to its Physics object.
See the properties listed below for more information.

SEE: https://zimjs.com/physics/ for examples

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// add a circle to a default Physics world,
// set it draggable and turn debug mode on
var circle = new Circle().center().addPhysics();
circle.physics.drag();
circle.physics.debug();
END EXAMPLE

EXAMPLE
// create a world with no gravity (viewed from top like air-hockey)
var physics = new Physics(0);
var circle = new Circle(50,blue,grey).center().addPhysics({restitution:1.1}); // how bouncy
// make sure to centerReg() any rectangular objects
var rect = new Rectangle(30,400).centerReg().pos(70).addPhysics(false); // static - do not move
var tri = new Triangle(150,150,150,green,grey).center().pos(200).addPhysics({linear:10});
physics.drag();
// test to see if circle hits rectangle
// contact callback function receives ZIM object (and physics body as next param)
circle.contact(function (obj) {
	if (obj == rect) {
		rect.color = red;
		timeout(300, function () {
			rect.color = black;
			// circle.noContact(); // could only check once...
		})
	}
});
END EXAMPLE

PARAMETERS - FOR PHYSICS
** supports DUO - parameters or single object with properties below
gravity - (default 10) the gravity force in the downward direction
borders - (default the frame bounds) a ZIM Boundary() object or object literal with x, y, width and height properties
	this makes physics body rectangles around the physics world
	pass in "none" for no borders
	you can remove border bodies afterwards with physics.remove(physics.borderTop)
	and then borderLeft, borderRight and borderBottom
scroll - (default false) set to true to be able to scroll the stage x an y
 	This will let you follow a physics object - see follow() method
frame - (default zimDefaultFrame) the zim.Frame object

METHODS - FOR PHYSICS - see also OBJECT METHODS and BODY METHODS below
borders(boundary) - (default frame bounds) a ZIM Boundary() object or an object literal {} with x, y, width and height
	pass in "none" for no borders - also see borders parameter
	will remove old borders bodies before making new
drag(array) - drag all physics objects (with dynamic set to true)
	array - (default null) pass in an array of objects to drag
	Note: physics dragging is applied on the Physics class to not overwrite traditional ZIM drag() on DisplayObjects
noDrag() - stops dragging all physics objects
join(obj1, obj2, point1, point2, minAngle, maxAngle, type) - creates and returns a physics joint
	obj1 - the first ZIM DisplayObject (with physics turned on) to join together
	obj2 - the second ZIM DisplayObject (with physics turned on) to join together
	point1 - (default center of object) the anchor point of the joint on the first object
	point2 - (default center of object) this is only needed on a distance joint
	minAngle - (default null) the minimum angle the joint can make from its starting angle
	maxAngle - (default null) the maximum angle the joint can make from its starting angle
	type - (default "weld") the type of joint
		set to "distance" to keep the same distance between two object anchors
		set to "resolute" to rotate objects around a fixed point relative to the first object
		set to "weld" to fix the objects together
break(joint) - break a joint created with join()
	to use, store the result of the join() method in a variable and pass that variable in to break()
debug() - activates the debugging
updateDebug() - updates the debug canvas if the frame has been scaled (put in frame.resize())
removeDebug() - removes the debug canvas - you can add it again later (or toggle, etc.)
// FOR BODIES ONLY (legacy - but may not need a visual ZIM object)
makeRectangle(width, height, ... all addPhysics() params) - makes a Box2D body in the shape of a rectangle - see parameters
makeCircle(radius, ... all addPhysics() params) - makes a Box2D body in the shape of a circle - see parameters
makeTriangle(a, b, c, ... all addPhysics() params) - makes a Box2D body in the shape of a triangle
	all three add x, y and rotation properties to body (use at start)
	all three support ZIM DUO single parameter as an object using param names as keys
remove(body) - removes a physics body - use on border bodies, for instance
	this is for physics bodies only - use obj.removePhysics() to remove physics from ZIM DisplayObjects
addMap(body, asset) - sets x, y, rotation of ZIM asset to Box2D body
	this can be automatically done by using the physics() method on the ZIM DisplayObject
removeMap(body) - removes mapping (then probably will want to remove body and removeChild)
	see the removePhysics() method of a ZIM DisplayObject
dispose() - stops the update, removes debug if there - you still need to remove ZIM assets
*** also see Ticker below for methods to add and remove functions from update function

PROPERTIES - FOR PHYSICS - see also OBJECT PROPERTIES and BODY PROPERTIES below
world - the Box2D world that is made
scale (read only) scale used in world (constant 30)
step (read only) step used in world (constant 20)
gravity (read only) gravity used in world (whatever parameter was passed)
Ticker - gives access to update function to add your own functions:
	physics.Ticker.add(function, after)
		after defaults to true for after world step and force clear
		set after to false to run function before world step
	physics.Ticker.remove(function)
controlObj - the ZIM DisplayObject being controlled by the obj.control() method
followObj - the ZIM DisplayObject being followed by the obj.follow() method
borderTop - the top border physics body of the world if it exists
borderBottom - the bottom border physics body of the world if it exists
borderLeft - the left border physics body of the world if it exists
borderRight - the right border physics body of the world if it exists

METHODS - FOR OBJECTS - also see BODY METHODS below
** the ZIM DisplayObject comes with an addPhyics() method
** for reference, here are the docs for the addPhysics() method
** all the methods below addPhysics are the methods added to the DisplayObject
addPhysics(dynamic, contract, shape, friction, linear, angular, density, restitution, maskBits, categoryBits, physics)
	** supports DUO - parameters or single object with properties below
	dynamic - (default true) - set to true to not move the physics body (static)
	contract - (default 0) - make the physics body smaller (or bigger with negative) than bounds
	shape - (default object shape) - "rectangle" for any object other than Cirlce, Dial and Triangle
	 	but can specify a "circle" for a Sprite or Bitmap, for instance - to try and match shape
		custom polygon bodies can also be made with manual Box2D and then use physics.setMap()
		but the only shapes available automatically are "rectangle", "circle", "triangle"
	friction - (default .8) - how sticky will the body act - set to 0 to slide.
	linear - (default .5) - linear damping which slows the movement - set to 0 for no damping
	angular - (default .5) - angular damping which slows the rotation - set to 0 for no damping
	density - (default 1) - the density that can affect what happens with collisions
	restitution - (default 0) - how bouncy the object is - 0 is not bouncy 4 is crazy bouncy
	maskBits - (default null) - used with categoryBits to determine which bodies will collide with which other bodies
		as soon as maskBits is specified, the body will collide only with the categoryBits provided to the maskBits parameter
		1 will collide with bodies that do not have categoryBits specified including the borders
		to test collision with bodies that have categoryBits specified, use the pipe (|) as follows:
		1|2 will also collide with bodies having categoryBits of 2 specified
		so if another body has categoryBits of 4 then the bodies would not collide.
		1|2|4 would also collide with 4 but not bodies with categoryBits of 8, etc.
		2|4 would pass through any bodies without categoryBits of 2 or 4 including the borders
	categoryBits - (default 1) - a collision category - by default, bodies are in category 1
		use with maskBits to say which bodies will collide with which other bodies
		the values are bit fields https://en.wikipedia.org/wiki/Bit_field so can have the following decimal values:
		can be 2, 4, 8, 16, 32, 64, 128, 256, etc. up to 15 powers of 2
	physics - (default zimDefaultPhysics)
removePhysics() - lets you remove the DisplayObject from the physics world
	to add back to the physics world, use the addPhysics() method
impulse(x, y, targetX, targetY) - add a one-time force on the object like a hitting a pool ball
	x - (default 0) the force in the x direction
	y - (default 0) the force in the y direction
	targetX - (default center of object) the x location on the object where the force acts
	targetY - (default center of object) the y location on the object where the force acts
force(x, y, targetX, targetY) - add a force over time on the object like a gravity or wind
	this is applied in a Ticker or on keydown, etc. many times
	just applying the force once will not do anything - use an impulse() for that
	x - (default 0) the force in the x direction
	y - (default 0) the force in the y direction
	targetX - (default center of object) the x location on the object where the force acts
	targetY - (default center of object) the y location on the object where the force acts
spin(amount) - add a one time turning force to an object
torque(amount) - add a turning force over time on the object
	this is applied in a Ticker or on keydown, etc. many times
	just applying the torque once will hardly do anything - use an spin() for that
sleep() - puts object to sleep so no physics calculations are done
wake() - wakes up object from sleep so physics calculations are done
follow(damp, dampY, leftOffset, rightOffset, upOffset, downOffset, offsetDamp, offsetDampY, horizontal, vertical, borderLock, borderOriginal)
	moves stage to to follow ZIM object
	set the scroll parameter of Physics to true
	pass in null for obj to stop following - then can position stage with x and y
	** supports DUO - parameters or single object with properties below
	damp - (default .05) the damping of the motion of the stage - 1 moves faster, 0 not at all
	dampY - (default damp) can set to damp vertical movement at a separate rate
	leftOffset - (default 0)
	rightOffset - (frame.width)
		the object will try and move to leftOffset when moving right and rightOffset when moving left
		this counters the damping so that the user can see in the direction of motion
		when the object is not being controled it moves to the average between left and right offsets
	upOffSet - (default 0)
	downOffSet - (default frame.height)
		same as offsets above but in the y direction
	offsetDamp - (default .02) the damping for moving the object to the offset
	offsetDampY - (default offsetDamp) - damping for moving the object to the y offset if desired to be different than x
	horizontal - (default true) set to false to not follow horizontally
	vertical - (default true) set to false to not follow vertically
	borderLock - (default true) locks follow to borders if borders are not "none"
	borderOriginal - (default false) set to true to lock follow to original borders even if removed
control(type, speed, speedY, horizontal, vertical) - control object with arrows
	see noControl() to turn off object control
	** supports DUO - parameters or single object with properties below
	type - (default "both") or set "wasd" or "arrows"
	speed - (default 200) the speed of the object motion
	speedY - (default speed) or set to specify a different y speed than the x speed
	horizontal - (default true) set to false to not control horizontal motion
	vertical - (default true) set to false to not control vertical motion
noControl() - remove control for an object set with control()
contact(call) - run the call function when object's body contacts another body
	the callback function receives two parameters - the ZIM object and the Physics body that the object has hit
	Note that the borders do not have ZIM Objects so use the second parameter to test against physics.borderBottom - etc.
contactEnd(call) - run the call function when object's body ends contacts with another body
	the callback function receives two parameters - the ZIM object and the Physics body that the object has hit
	Note that the borders do not have ZIM Objects so use the second parameter to test against physics.borderBottom - etc.
noContact() - remove contact call
noContactEnd() - remove contactEnd call

PROPERTIES - FOR OBJECTS - see also BODY PROPERTIES below
dynamic - set to true for dynamic and false for static
	there is also kinematic that can be set using the obj.body.SetType(1)
** normal x, y, rotation or pos(), loc(), rot() will not work with physics!
** see the BODY PROPERTIES below for x, y and rotation
** these should really not be set at all in the physics world
** but rather let the forces, etc. work them out
** it is best to set traditional properties before calling physics()

METHODS - FOR BODY (a phsyics engine body)
** the ZIM DisplayObject body property provides access to the following Box2D methods (note, all start with uppercase):
** commonly used methods are handled through ZIM wrapper methods on the DisplayObject
** see http://www.box2dflash.org/docs/2.1a/reference/ for very basic docs
Advance() ApplyForce() ApplyImpulse() ApplyTorque() CreateFixture() CreateFixture2() DestroyFixture()
GetAngle() GetAngularDamping() GetAngularVelocity() GetContactList() GetControllerList() GetDefinition()
GetFixtureList() GetInertia() GetJointList() GetLinearDamping() GetLinearVelocity()
GetLinearVelocityFromLocalPoint() GetLinearVelocityFromWorldPoint() GetLocalCenter() GetLocalPoint(
GetLocalVector() GetMass() GetMassData() GetNext() GetPosition() GetTransform() GetType() GetUserData()
GetWorld() GetWorldCenter() GetWorldPoint() GetWorldVector() IsActive() IsAwake() IsBullet() IsFixedRotation()
IsSleepingAllowed() Merge() ResetMassData() SetActive() SetAngle() SetAngularDamping() SetAngularVelocity()
SetAwake() SetBullet() SetFixedRotation() SetLinearDamping() SetLinearVelocity() SetMassData() SetPosition()
SetPositionAndAngle() SetSleepingAllowed() SetTransform() SetType() SetUserData() ShouldCollide()
Split() SynchronizeFixtures() SynchronizeTransform() b2Body() connectEdges()

PROPERTIES - FOR BODY (a phsyics engine body)
zimObj - the ZIM Object that the body is mapped to
** traditional properties should be set before calling phyics()
** but the following properties are provided
x - the x position of the body - setting this will also position of the ZIM DisplayObject
y - the y position of the body - setting this will also position of the ZIM DisplayObject
rotation - the rotation of the body (degrees) - setting this will also rotate the ZIM DisplayObject

GLOBAL VARIABLES
** Making a new Physics() or using physics() on a ZIM DisplayObject
** gives global shortcut access to the following Box2D classes:
** these all have methods and properties http://www.box2dflash.org/docs/2.1a/reference/
b2Vec2 = Box2D.Common.Math.b2Vec2;
b2BodyDef = Box2D.Dynamics.b2BodyDef;
b2Body = Box2D.Dynamics.b2Body;
b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
b2Fixture = Box2D.Dynamics.b2Fixture;
b2World = Box2D.Dynamics.b2World;
b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef;
b2DistanceJointDef = Box2D.Dynamics.Joints.b2DistanceJointDef;
b2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef;
b2WeldJointDef = Box2D.Dynamics.Joints.b2WeldJointDef;
b2AABB = Box2D.Collision.b2AABB;
b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
b2BuoyancyController = Box2D.Dynamics.Controllers.b2BuoyancyController;
b2ContactListener = Box2D.Dynamics.b2ContactListener;
--*///+69.97
	zimContactListener = null; // global so dispose() in physics can access this
	var zimContactBeginList;
	var zimContactEndList;
	function setPhysics(obj, dynamic, contract, shape, friction, linear, angular, density, restitution, maskBits, categoryBits) {
		z_d("69.97");

		if (zot(contract)) contract = 0;
		if (shape == "rectangle") obj.body = obj.physics.makeRectangle(obj.width-contract, obj.height-contract, dynamic, friction, angular, density, restitution, maskBits, categoryBits, linear);
		else if (shape == "circle") obj.body = obj.physics.makeCircle(obj.width/2-contract, dynamic, friction, angular, density, restitution, maskBits, categoryBits, linear);
		else if (shape == "triangle") {
			if (zot(obj.a)) {
				obj.a = obj.width;
				obj.b = obj.c = Math.sqrt(Math.pow(obj.a/2,2) + Math.pow(obj.height,2));
			}
			obj.body = obj.physics.makeTriangle(obj.a-contract, obj.b-contract, obj.c-contract, dynamic, friction, angular, density, restitution, maskBits, categoryBits, linear);
		}

		obj.body.x = obj.x;
		obj.body.y = obj.y;
		obj.body.rotation = obj.rotation;
		obj.physics.addMap(obj.body, obj);

		obj.impulse = function(x, y, targetX, targetY) {
			if (zot(x)) x = 0;
			if (zot(y)) y = 0;
			var defaultTarget = obj.body.GetWorldCenter();
			if (zot(targetX)) targetX = defaultTarget.x*obj.physics.scale;
			if (zot(targetY)) targetY = defaultTarget.y*obj.physics.scale;
			obj.body.ApplyImpulse(new b2Vec2(x, y), new b2Vec2(targetX/obj.physics.scale, targetY/obj.physics.scale));
			return obj;
		}
		obj.force = function(x, y, targetX, targetY) {
			if (zot(x)) x = 0;
			if (zot(y)) y = 0;
			var defaultTarget = obj.body.GetWorldCenter();
			if (zot(targetX)) targetX = defaultTarget.x*obj.physics.scale;
			if (zot(targetY)) targetY = defaultTarget.y*obj.physics.scale;
			obj.body.ApplyForce(new b2Vec2(x, y), new b2Vec2(targetX/obj.physics.scale, targetY/obj.physics.scale));
			return obj;
		}
		obj.spin = function(amount) {
			if (zot(amount)) amount = 10;
			obj.body.ApplyTorque(amount*1000);
			return obj;
		}
		obj.torque = function(amount) {
			if (zot(amount)) amount = 10;
			obj.body.ApplyTorque(amount);
			return obj;
		}

		obj.follow = function(damp, dampY, leftOffset, rightOffset, upOffset, downOffset, offsetDamp, offsetDampY, horizontal, vertical, borderLock, borderOriginal) {
			var sig = "damp, dampY, leftOffset, rightOffset, upOffset, downOffset, offsetDamp, offsetDampY, horizontal, vertical, borderLock, borderOriginal";
			var duo; if (duo = zob(obj.follow, arguments, sig)) return duo;
			obj.physics.follow(obj, damp, dampY, leftOffset, rightOffset, upOffset, downOffset, offsetDamp, offsetDampY, horizontal, vertical, borderLock, borderOriginal);
			return obj;
		}
		obj.noFollow = function() {
			obj.physics.follow(null);
			return obj;
		}

		obj.control = function(type, speed, speedY, horizontal, vertical) {
			var sig = "type, speed, speedY, horizontal, vertical";
			var duo; if (duo = zob(obj.control, arguments, sig)) return duo;
			obj.physics.control(obj, type, speed, speedY, horizontal, vertical);
			return obj;
		}
		obj.noControl = function() {
			obj.physics.noControl(obj);
			return obj;
		}

		obj.sleep = function() {
			zog(obj.body)
			obj.body.SetAwake(false);
			return obj;
		}
		obj.wake = function() {
			obj.body.SetAwake(true);
			return obj;
		}

		function makeContact() {
			zimContactListener = new b2ContactListener();
			zimContactBeginList = new zim.Dictionary(true);
			zimContactEndList = new zim.Dictionary(true);
			zimContactListener.BeginContact = beginContact;
			zimContactListener.EndContact = endContact;
			function beginContact(e) {
				var match = zimContactBeginList.at(e.m_fixtureB.GetBody());
				if (match) match(e.m_fixtureA.GetBody().zimObj, e.m_fixtureA.GetBody());
				else {
					match = zimContactBeginList.at(e.m_fixtureA.GetBody());
					if (match) match(e.m_fixtureB.GetBody().zimObj, e.m_fixtureB.GetBody());
				}
			}
			function endContact(e) {
				var match = zimContactEndList.at(e.m_fixtureB.GetBody());
				if (match) match(e.m_fixtureA.GetBody().zimObj, e.m_fixtureA.GetBody());
				else {
					match = zimContactEndList.at(e.m_fixtureB.GetBody().zimObj, e.m_fixtureB.GetBody());
					if (match) match(e.m_fixtureB.GetBody());
				}
			}
			obj.physics.world.SetContactListener(zimContactListener);
		}
		obj.contact = function(f) {
			if (!zimContactListener) makeContact();
			zimContactBeginList.add(obj.body, f);
			return obj;
		}
		obj.noContact = function() {
			zimContactBeginList.remove(obj.body);
			return obj;
		}
		obj.contactEnd = function(f) {
			if (!zimContactListener) makeContact();
			zimContactEndList.add(obj.body, f);
			return obj;
		}
		obj.noContactEnd = function() {
			zimContactEndList.remove(obj.body);
			return obj;
		}
		Object.defineProperty(obj, 'dynamic', {
			get: function() {
				return obj.body.GetType() == 2;
			},
			set: function(val) {
				obj.body.SetType(val==true?2:0);
			}
		});
	}
	//-69.97

// SUBSECTION EFFECTS

/*--
zim.Parallax = function(layers, damp, auto, stage, startPaused, mouseMoveOutside, clamp)

Parallax
zim class

DESCRIPTION
Takes objects as layers and sets properties based on an input,
for instance, each layer could move a different x based on position of mouseX
or each layer could scale a different amount based on scroll of y.
The types of input are mouseX, mouseY, scrollX, scrollY or custom.
The types of properties to change could be x, y, scaleX, scaleY, rotation, alpha, frameNumber, etc.
Parallax allows scale to be a property which scales scaleX and scaleY together.
Parallax allows frame to be a property and calls gotoAndStop() on a Sprite frame.
Parallax really just manages multiple ProportionDamp objects.
For proper parallax, the objects closer move more than the objects farther back.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// make assets to move around
// these could be pictures, shapes, containers, etc.
var backing = new Rectangle(800, 200, "yellow");
backing.center(stage);
var mid = new Rectangle(400, 200, "green");
mid.center(stage).y += 20;
var front = new Circle(60, "red");
front.center(stage).y += 80;

// make Parallax object - here we move with stage mouseX and mouseY
var parallax = new Parallax([
	{obj:backing, prop:"x", propChange:50}, {obj:backing, prop:"y", propChange:40, input:"mouseY"},
	{obj:mid, prop:"x", propChange:100}, {obj:mid, prop:"y", propChange:80, input:"mouseY"},
	{obj:front, prop:"x", propChange:150}, {obj:front, prop:"y", propChange:100, input:"mouseY"}
], .1);

stage.update();
END EXAMPLE

EXAMPLE
// interact outside a stage
// this example has a small stage in the middle of the HTML page
// see https://zimjs.com/explore/parallax.html
var parallax = new Parallax({mouseMoveOutside:true, startPaused:true});
var colors = series(black, white);
new Rectangle(stageW, stageH).center()
loop(10, function (i, total) {
	var rect = new Rectangle(stageW*(1-i/10), stageH*(1-i/10), colors()).center();
	parallax.addLayer({
		obj:rect,
		input:"mouseX", // default so don't need...
		prop:"x", // default so don't need...
		propChange:40*i,
		inMin:-1000,
		inMax:stageW+1000
	});
	parallax.addLayer({
		obj:rect,
		input:"mouseY",
		prop:"y",
		split:true,
		propChange:40*i,
		inMin:-500,
		inMax:stageH+500
	});
});

// starting the parallax paused so this will unpause when mouse is over stage
stage.on("stagemousemove", function (e) {
	if (stage.mouseInBounds) { // now that state is set to move outside...
		// go immediately to the target input - needs to be done for each layer
		var targets = [];
		loop(10, function (i) {
			targets.push(e.stageX, e.stageY)
		});
		parallax.immediate(targets); // pass in the array
		parallax.pause(false); // start the parallax
		e.remove(); // end event
	}
});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
layers - (default null) an array of layer objects, the format as below
	Example: to move an obj 200 px in the x as the window scrolls from 100 to 300 px in the y
		[{obj:obj, prop:"x", propChange:200, input:"scrollY", inMin:100, inMax:300, factor:1, integer:false}, etc.]
	obj - the object whose property is being changed
	prop - the property that is being changed
	propChange - how much you want the property to change
	input - (default mouseX) but can also be mouseY, scrollX, scrollY
	inMin - (default 0) minimum input range
	inMax - (default stageW (for x prop) stageH (for y prop)) maximum input range
	factor - (default 1) set factor to -1 to change in the opposite direction
	integer - (default false) set to true to round the value to an integer
	split - (default true for mouseX, false for others) centers input so half output is on one side and half on the other
	immediate - (default null) set to a value to set property immediately take that value without damping from current property value
	Example 2: a traditional mouse move parallax for one object
		[{obj:obj, prop:"x", propChange:100}, {obj:obj, prop:"y", propChange:50, input:"mouseY"}, etc.]
	you would probably have more objects to follow
	or you can add these one at a time with the p.addLayer({layer object properties});
damp - (default .1) the damp value with 1 being no damping and 0 being no movement
auto - (default true) uses the specified input
	if auto is set to false, you must make your own Ticker and use the step(input) method
stage - (default the default frame's stage) the stage - specify this if multiple stages
startPaused - (default false) set to true to start parallax paused - set paused(false) to unpause it
mouseMoveOutside - (default true) set to false to not allow mouse movement outside the stage to affect motion
clamp - (default true) set to false to allow range outside min and max
NOTE: ticker and fps parameters have been removed - see Ticker to set

METHODS
addLayer({layer object properties}) - adds a layer
removeLayer(index) - removes a layer based on order added
step(input) - used when auto is false to send in custom input data
immediate([]) - an array of inputs for each layer used to immediately set the target value with no damping
pause(state) - pauses or unpauses the parallax - state defaults to true - see also startPaused
dispose() - removes listeners

PROPERTIES
type - holds the class name as a String
damp - allows you to dynamically change the damping
paused - read-only boolean whether the parallax is paused - see pause() and startPaused property
--*///+68
	zim.Parallax = function(layers, damp, auto, stage, startPaused, mouseMoveOutside, clamp) {
		var sig = "layers, damp, auto, stage, startPaused, mouseMoveOutside, clamp";
		var duo; if (duo = zob(zim.Parallax, arguments, sig, this)) return duo;
		z_d("68");
		this.type = "Parallax";

		if (zot(stage)) {
			if (layers && layers[0] && layers[0].obj && layers[0].obj.stage) {
				stage = layers[0].obj.stage;
			} else if (zimDefaultFrame) {
				stage = zimDefaultFrame.stage;
			} else {
				return;
			}
		}
		if (zot(auto)) auto = true;
		else auto = false;
		if (zot(startPaused)) startPaused = false;
		if (zot(mouseMoveOutside)) mouseMoveOutside = false;

		var stageW = stage.getBounds().width;
		var stageH = stage.getBounds().height;
		stage.mouseMoveOutside = mouseMoveOutside;

		var that = this;
		stage.on("stagemousemove", function (e) {
			that.mouseX = e.rawX/stage.scaleX;
			that.mouseY = e.rawY/stage.scaleY;
		});

		// public properties
		var _damp = (zot(damp)) ? .1 : damp;

		// public methods (do not get hoisted so define early)
		// addLayer works as a public method
		// and also is called from the object in case we add layers via the Parallax object parameters
		// the function prepares ProportionDamp objects for two values
		// and stores them on the layer object
		// and also stores the desired amounts on the layer objects themselves
		// finally, the layer object is added to the myLayers private property
		// the timer then loops through these layers and handles things from there
		// obj, distanceX, distanceY, minX, minY, maxX, maxY, factor, integer, split
		this.addLayer = function(layer) {
			//{obj, prop, propChange, input, inMin, inMax, factor, integer}
			if (zot(layer.obj) || zot(layer.prop) || zot(layer.propChange)) return;
			var obj = {obj:layer.obj, prop:layer.prop};
			obj[obj.prop] = layer.propChange;
			if (zot(layer.input)) layer.input = "mouseX";
			obj.input = layer.input;
			obj.split = zot(layer.split) ? ((layer.input == "mouseX" && auto) ? true : false) : layer.split;

			var inMin = (zot(layer.inMin)) ? 0 : layer.inMin;
			var inMax = (zot(layer.inMax)) ? ((layer.input == "mouseX" || layer.input == "scrollX") ? stageW : stageH) : layer.inMax;
			var factor = (zot(layer.factor)) ? 1 : layer.factor;
			var integer = (zot(layer.integer)) ? false : layer.integer;

			// baseMin, baseMax, targetMin, targetMax, damp, factor, targetRound, clamp
			obj["p_"+obj.prop] = new zim.ProportionDamp(inMin, inMax, 0, obj[obj.prop], _damp, factor, integer, clamp);
			if (obj.prop == "scale") {
				obj["s_"+obj.prop] = obj.obj.scaleX; // helper to allow scale to be property
			} else if (obj.prop == "frame") {
				obj["s_"+obj.prop] = obj.obj.currentFrame;
			} else {
				obj["s_"+obj.prop] = obj.obj[obj.prop]; // obj.s_x = obj.obj.x for example
			}
			if (!zot(layer.immediate)) obj["p_"+obj.prop].immediate(layer.immediate);
			myLayers.push(obj);
			return myLayers.length-1;
		}

		this.removeLayer = function(index) {
			if (zot(index)) return;
			var layer = myLayers[index];
			layer["p_"+layer.prop].dispose();
			myLayers.splice(index,1);
		}

		this.immediate = function(array) {
			var o;
			for (var i=0; i<myLayers.length; i++) {
				o = myLayers[i];
				o["p_"+o.prop].immediate(array[i]);
			}
		}

		this.dispose = function() {
			myLayers = null;
			if (auto) zim.Ticker.remove(zimTicker);
			return true;
		}

		// private properties
		// here are any layers that come in from Parallax object parameters
		layers = (zot(layers)) ? [] : layers;

		// we now are going to process these layers with the public addLayer method
		// this will add the processed layers to the private property, myLayers
		var myLayers = [];
		for (var i=0; i<layers.length; i++) {
			this.addLayer(layers[i]);
		}

		if (auto) {
			var zimTicker = zim.Ticker.add(animate, stage);
			if (startPaused) zim.Ticker.remove(zimTicker);
		}

		// loop though our layers and apply the converted proportion damping
		function animate(e) {
			that.step();
		}

		this.step = function(custom) {
			var o; var input;
			for (var i=0; i<myLayers.length; i++) {
				o = myLayers[i];
				if (zot(custom)) {
					if (o.input == "mouseX") input = that.mouseX;
					else if (o.input == "mouseY") input = that.mouseY;
					else if (o.input == "scrollX") input = zim.scrollX();
					else if (o.input == "scrollY") input = zim.scrollY();
				} else {
					input = custom;
				}
				// damp object at property to start value + converted goal based on input
				if (o.prop == "scale") {
					o.obj.scaleX = o.obj.scaleY = o["s_"+o.prop] + o["p_"+o.prop].convert(input);
				} else if (o.prop == "frame") {
					o.obj.gotoAndStop(o["s_"+o.prop] + o["p_"+o.prop].convert(input));
				} else {
					o.obj[o.prop] = o["s_"+o.prop] + o["p_"+o.prop].convert(input);
					// for x on mouseX we split the destination range in two for a centered parallax
					// if (o.input == "mouseX" && auto) o.obj[o.prop] -= o[o.prop] / 2;
					if (o.split) o.obj[o.prop] -= o[o.prop] / 2;
				}
			}
		}

		that.paused = startPaused;
		that.pause = function(state) {
			if (zot(state)) state = true;
			if (that.paused != state) {
				that.paused = state;
				if (zimTicker) {
					if (!state) zimTicker = zim.Ticker.add(zimTicker);
					else zim.Ticker.remove(zimTicker);
				}
			}
 		}


		Object.defineProperty(that, 'damp', {
			get: function() {
				return _damp;
			},
			set: function(value) {
				_damp = value;
				var o;
				for (var i=0; i<myLayers.length; i++) {
					o = myLayers[i];
					o["p_"+o.prop].damp = _damp;
				}
			}
		});

	}//-68


/*--
zim.Scroller = function(backing, speed, direction, horizontal, gapFix, stage, container, style, group, inherit)

Scroller
zim class extends a createjs.EventDispatcher

DESCRIPTION
Scroller animates a backing either horizontally or vertically (not both).
The Scroller object will create a clone of the backing
and animate and swap the backgrounds when needed.

NOTE: A scroller can be added to a Accelerator object
this will allow the percentSpeed to be synched with other Scroller and Dynamo objects
See https://zimjs.com/zide/
See https://zimjs.com/explore/sidescroller.html (with keys)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var one = new Rectangle(1200, 400, "red");
frame.makeCircles().center(one);
stage.addChild(one);

var scroller = new Scroller(one);

stage.update();
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
backing - an object to animate (make start and end edges match to be seemless)
speed - (default 1) how fast in pixels per second the animation is going
direction - (default 1) set to -1 for left or down
horizontal - (default true) set to false to animate vertically
	you can adjust the speed and direction properties dynamically
	you cannot adjust the backings and horizontal dynamically
	to change your animation, dispose() of the Scroller object and make a new one
	disposing just removes the ticker - you have to remove the backings
	NOTE: the gapFix and ticker parameters have been removed - see Ticker
gapFix - (default 0) if a thin line appears when changing speed - try setting to 1 or 2
stage - (default background.stage) if the backround is not on the stage then need to pass the stage it will be on
container - (default stage) what bounds are used for wrapping the background

METHODS
pause(state) - state defaults to true and pauses the scroller (sets speed to 0)
	set state to false to unpause the scroller (sets speed to speed before pausing)
dispose() - get rid of the event listeners - you need to remove the backings (see backing properties)

PROPERTIES
type - holds the class name as a String
backing1 - the original backing passed in
backing2 - the cloned backing made from the original backing
speed - how fast the animation is going in pixels per frame
baseSpeed - the scroller speed when it was first made (or can override)
	used to determine percentage speed for percentSpeed property
percentSpeed - get or set the percentage of the baseSpeed - eg. 100 is at the baseSpeed
	this allows you to animate multiple scrollers relative to one another
	See ScrollerManager class
direction - 0 for no direction, either left (-1) or right (1) if horizontal or up (-1) or down (1) if not horizontal
paused - read only - true if paused and false if not - must be set with pause() method

EVENTS
Dispatches a "pause" event when paused is complete (sometimes a delay to slow to pause)
--*///+69
	zim.Scroller = function(backing, speed, direction, horizontal, gapFix, stage, container, style, group, inherit) {
		var sig = "backing, speed, direction, horizontal, gapFix, stage, container, style, group, inherit";
		var duo; if (duo = zob(zim.Scroller, arguments, sig, this)) return duo;

		z_d("69");
		this.cjsEventDispatcher_constructor();
		this.type = "Scroller";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(backing)) backing = DS.backing!=null?DS.backing:null;
		if (zot(backing)) return;

		var b1 = this.backing1 = backing;
		if (zot(b1) || !b1.getBounds) return;
		var b2 = this.backing2 = backing.clone();
		if (zot(horizontal)) horizontal = DS.horizontal!=null?DS.horizontal:true;
		if (zot(gapFix)) gapFix = DS.gapFix!=null?DS.gapFix:0;
		if (zot(container)) container = DS.container!=null?DS.container:null;
		if (zot(stage)) stage = DS.stage!=null?DS.stage:null;

		var that = this;

		// here are the public properties that can be changed
		if (zot(speed)) speed = DS.speed!=null?DS.speed:1;
		this.speed = speed;
		var lastSpeed = this.baseSpeed = this.speed;
		if (zot(direction)) direction = DS.direction!=null?DS.direction:1;
		this.direction = direction;
		var scale = horizontal ? b1.scaleX : b1.scaleY;


		if (!b1.getBounds()) {
			zog("zim display - Scroller(): please setBounds() on backing objects");
			return;
		}
		if (!stage && !b1.stage) {
			zog("zim display - Scroller(): please pass in stage parameter or add backing objects to stage to start");
			return;
		}
		if (!b1.parent) {
			zog("zim display - Scroller(): please add object to container or stage before adding to Scroller");
			return;
		}
		b1.parent.addChildAt(b2, b1.parent.getChildIndex(b1));
		stage = stage||b1.stage;
		if (zot(container)) container = stage;
		if (!container.getBounds()) {zog("zim display - Scroller(): please setBounds() on container or stage if no container"); return;}

		var w = b1.getBounds().width*scale-gapFix;
		var h = b1.getBounds().height*scale-gapFix;

		var viewW;
		var viewH;

		if (horizontal) {
			b2.x = w;
		} else {
			b2.y = h;
		}

		var pausing = false; // for in the act of pausing
		var zimTicker = zim.Ticker.add(animate, stage);

		function animate(e) {
			if (!viewW) {
				viewW = container.getBounds().width;
				viewH = container.getBounds().height;
			}
			// pausing the ticker does not really pause the ticker (weird)
			if (that.speed == 0 || that.direction == 0) {return;}

			if (horizontal) {
				b1.x -= that.speed*that.direction;
				if (b1.x < b2.x) {
					b2.x = b1.x + w;
				} else {
					b2.x = b1.x - w;
				}
				if (that.direction * that.speed > 0) {
					if (b2.x < 0 && b1.x < b2.x) {
						b1.x = b2.x + w;
					} else if (b1.x < 0 && b2.x < b1.x) {
						b2.x = b1.x + w;
					}
				} else {
					if (b2.x > viewW && b2.x > b1.x) {
						b2.x = b1.x - w;
					} else if (b1.x > viewW && b1.x > b2.x) {
						b1.x = b2.x - w;
					}
				}
			} else {
				b1.y -= that.speed*that.direction;
				if (b1.y < b2.y) {
					b2.y = b1.y + h;
				} else {
					b2.y = b1.y - h;
				}
				if (that.direction * that.speed > 0) {
					if (b2.y < 0 && b1.y < b2.y) {
						b1.y = b2.y + h;
					} else if (b1.y < 0 && b2.y < b1.y) {
						b2.y = b1.y + h;
					}
				} else {
					if (b2.y > viewH && b2.y > b1.y) {
						b2.y = b1.y - h;
					} else if (b1.y > viewH && b1.y > b2.y) {
						b1.y = b2.y - h;
					}
				}
			}
		}

		this.paused = false;
		this.pause = function(state, time) {
			if (zot(state)) state = true;
			if (zot(time)) time = 0;
			if (state) {
				lastSpeed = that.speed;
				if (time > 0) {
					pausing = true;
					zim.animate({target:that, obj:{pausingSpeed:0}, ticker:false, time:time, call:function() {
						that.speed = 0;
						that.paused = true;
						pausing = false;
						that.dispatchEvent("pause");
					}});
				} else {
					pausing = false;
					that.speed = 0;
					that.paused = true;
					setTimeout(function() {that.dispatchEvent("pause");}, 10);
				}
			} else {
				pausing = false;
				if (time > 0) {
					zim.animate({target:that, obj:{pausingSpeed:lastSpeed}, ticker:false, time:time, call:function() {
						that.speed = lastSpeed;
						that.paused = false;
						pausing = false;
					}});
				} else {
					that.speed = lastSpeed;
					that.paused = false;
				}
			}
			return that;
		}

		Object.defineProperty(that, 'percentSpeed', {
			get: function() {
				if (that.baseSpeed == 0) return NaN;
				return that.speed / that.baseSpeed * 100;
			},
			set: function(percent) {
				if (pausing || that.paused) return;
				that.speed = that.baseSpeed * percent / 100;
			}
		});

		Object.defineProperty(that, 'pausingSpeed', {
			get: function() {
				if (that.baseSpeed == 0) return NaN;
				return that.speed / that.baseSpeed * 100;
			},
			set: function(percent) {
				that.speed = that.baseSpeed * percent / 100;
			}
		});

		if (style!==false) zimStyleTransforms(this, DS);

		this.dispose = function() {
			if (zon) zog("bye from Scroller");
			zim.Ticker.remove(zimTicker);
			return true;
		}
	}
	zim.extend(zim.Scroller, createjs.EventDispatcher, null, "cjsEventDispatcher");
	//-69


/*--
zim.Dynamo = function(sprite, speed, label, startFrame, endFrame, update, reversible, flip, flipVertical, style, group, inherit)

Dynamo
zim class - extends a createjs EventDispatcher

DESCRIPTION
A Dynamo can run any Sprite animation at varying speeds
You pass in an optional label, or start and end frames to define the animation frames
You can animate a Dynamo using speed or percentSpeed
percentSpeed is handy for animating at speeds relative to other animations and scrollers
You can control Dynamo speeds with mouse position - or in a Parallax object
A Dynamo loops automatically - you can pause it (with optional slowing or optional frame) and unpause it (with optional quickening)
You can also get or set its frame property at which point, it will loop from there (unless paused)
A Dynamo dispatches a "change" event every time the frame changes
and a loop event every time it loops to the start and a paused event when paused

NOTE: A Dynamo requires constant stage.update() to run the Sprite
A Ticker.add(function () { // speed Code}) or just Ticker.always() will provide the update

NOTE: A Dynamo can be added to a Accelerator object (and then to a MotionController)
this will allow the percentSpeed to be synched with other Scroller and Dynamo objects
See https://zimjs.com/zide/
See https://zimjs.com/explore/sidescroller.html

NOTE: Dynamo is an alternative to a Sprite.run() where you provide a set time for animation
but you can pause a Dynamo and then use run() and then unpause the Dynamo when the run is done
If you are controlling the Dynamo in a Ticker.add() function,
then make sure to remove() the Ticker function when the Dynamo is paused

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// *** the Sprite will not run with a Dynamo unless there is a constant stage.update()
// The Ticker will provide a constant stage.update() so that is good!

// we have a sprite of a guy and it has a "walk" animation
// we can make this run faster and slower with an accelerator:
// we pass in a speed of 30 fps and this becomes the baseSpeed

// do not rely on stage.mouseX - does not work on touchscreen!
var stageX;
stage.on("stagemousemove", function (e) {stageX = e.stageX;});
var dynamo = new Dynamo({sprite:sprite, speed:30, label:"walk", reversible:false});
Ticker.add(function() {
	// the sprite will run at 0 speed when the cursor is at the left of the stage
	// and get faster as the cursor moves to the right
	// at the middle it will be 30 fps and at the right it will be 60 fps
	dynamo.percentSpeed = stageX/stageW*100*2;
}, stage);

Here we apply damping and make the sprite play backwards at the left of half stage
var dynamo = new Dynamo(sprite, 30, "walk");
// do not rely on stage.mouseX - does not work on touchscreen!
var stageX;
stage.on("stagemousemove", function (e) {stageX = e.stageX;});
Ticker.add(function() {
	// will play backwards at 30 fps at left and forwards at 30 fps at right
	// it will stop at half the stage width
	// reversible false means it will not walk backwards
	// but rather it will flip and walk in the left direction when the speed is negative
	dynamo.percentSpeed = stageX/stageW*200 - 100;
}, stage);
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
sprite - the sprite to control
speed - (default 30) the frames per second at which to animate the sprite
label - (default null) the label of the sprite to play (see Sprite)
startFrame - (default 0) the frame to start the animation (ignored if a label is provided)
endFrame - (default sprite.totalFrames) the frame to end the animation (ignored if a label is provided)
update - (default false) set to true to update the stage (only do this if you are not already updating the stage!)
reversible - (default true) will allow percentSpeed to be negative and reverse the animation.  Set to false to use absolute value.
flip - (default true if reversible is false) will flip the scaleX of the sprite if speed is negative and reversible is set to false.
	the pairing of reversible false and flip true will make a Sprite turn and walk the other way if the speed is negaitive
	Note: also see the scaleX property
flipVertical - (default false) flip the Sprite in the vertical if the speed is negative (note also see the scaleY property)

METHODS
pause(state, time, frame) - the way to pause or unpause a Dynamo affecting the sprite animating
	state - (default true) true pauses and setting the state to false will unpause the animation
	time - (default 0) time in milliseconds to slow the animation down if pausing or speed it up if unpausing
	frame - (default null) which frame to pause on - overrides time (unless you want to do the calculation...)
dispose() - cancels the requestAnimationFrame

PROPERTIES
type - holds the class name as a String
frames - an array of frame numbers the Dynamo is acting on according to label, or startFrame, endFrame
frame - the current frame of the Dynamo - this is sequential relative to frames
	whereas the actual Sprite frame may be different as labels can specify non-consecutive frame numbers
totalFrames - the total frames in frames (may be different than the Sprite's total frames)
percentSpeed - get or set the percentage of the baseSpeed - eg. 100 is a the baseSpeed
	this is what you should animate to speed up and slow down the sprite
	this allows you to set the speed relative to other Sprites and Scrollers
speed - get or set the speed of the sprite in frames per second
baseSpeed - the start speed given in frames per second unless changed with this property
	this affects the percentSpeed so usually it is not adjusted - but it can be
paused - read only - whether the Dynamo is paused or not (by using the pause() method)
scaleX - starts with the original scaleX of the Sprite
	if you flip the sprite and are scaling the Sprite manually, then also set the scaleX of the dynamo to match
scaleY - starts with the original scaleY of the Sprite
	if you flip the sprite and are scaling the Sprite manually, then also set the scaleY of the dynamo to match

EVENTS
dispatches a "change" event when the Dynamo changes frame
dispatches a "loop" event when the Dynamo loops (possibly in reverse)
dispatches a "pause" event when the Dynamo is paused - could be delayed
--*///+69.2
	zim.Dynamo = function(sprite, speed, label, startFrame, endFrame, update, reversible, flip, flipVertical, style, group, inherit) {
		var sig = "sprite, speed, label, startFrame, endFrame, update, reversible, flip, flipVertical, style, group, inherit";
		var duo; if (duo = zob(zim.Dynamo, arguments, sig, this)) return duo;

		z_d("69.2");
		this.cjsEventDispatcher_constructor();
		this.type = "Dynamo";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(sprite)) sprite = DS.sprite!=null?DS.sprite:null;
		if (zot(sprite)) return;


		var frames = this.frames = sprite.parseFrames(label, startFrame, endFrame, true); // last true is fromDynamo
		if (frames.length == 0) return;
		this.totalFrames = frames.length;
		var _frame = 0; // frame for getter and setter methods
		if (zot(speed)) speed = DS.speed!=null?DS.speed:30;
		if (zot(reversible)) reversible = DS.reversible!=null?DS.reversible:true;
		var lastSpeed = this.baseSpeed = this.speed = speed;
		if (zot(update)) update = DS.update!=null?DS.update:false;
		if (zot(flip)) flip = DS.flip!=null?DS.flip:null;
		if (zot(flip) && reversible == false) flip = true;

		var that = this;
		var requestID;
		var speedFactor;
		var lastTime = Date.now();
		var currentTime;
		var wait;
		var endFrameRequest;
		var pausing = false; // for in the act of pausing
		that.scaleX = sprite.scaleX;
		that.scaleY = sprite.scaleY;
		function doDynamo() {
			requestID = requestAnimationFrame(doDynamo);
			speedFactor = frames[_frame].s;
			if (that.speed == 0 || speedFactor == 0) return;
			wait = 1000/Math.abs(that.speed)*speedFactor;
			currentTime = Date.now();
			if (currentTime - lastTime > wait) {
				lastTime = currentTime;
				var nextFrame = that.frame+((that.speed>0 || !reversible)?1:-1);
				var loopCheck = false;
				if (nextFrame >= frames.length) {loopCheck = true; nextFrame = 0;}
				if (nextFrame < 0) {loopCheck = true; nextFrame = frames.length-1;}
				that.frame = nextFrame;
				if (flip) {
					if (that.speed < 0) sprite.scaleX = -that.scaleX;
					else sprite.scaleX = that.scaleX;
				}
				if (flipVertical) {
					if (that.speed < 0) sprite.scaleY = -that.scaleY;
					else sprite.scaleY = that.scaleY;
				}
				if (loopCheck) that.dispatchEvent("loop");
				that.dispatchEvent("change");
				if (update && sprite.stage) sprite.stage.update();
				if (!zot(endFrameRequest) && endFrameRequest == that.frame) {
					pausing = false;
					that.speed = 0;
					that.paused = true;
					that.dispatchEvent("pause");
				}
			}
		}
		doDynamo();

		this.paused = false;
		this.pause = function(state, time, frame) {
			if (zot(state)) state = true;
			if (zot(time)) time = 0;
			if (state) {
				lastSpeed = that.speed;
				if (zot(frame)) {
					if (time > 0) {
						pausing = true;
						zim.animate({target:that, obj:{pausingSpeed:0}, ticker:false, time:time, call:function() {
							pausing = false;
							that.speed = 0;
							that.paused = true;
							that.dispatchEvent("pause");
						}});
					} else {
						pausing = false;
						that.speed = 0;
						that.paused = true;
						setTimeout(function() {that.dispatchEvent("pause");}, 10);
					}
				} else {
					pausing = true;
					endFrameRequest = frame;
				}
			} else {
				endFrameRequest = null;
				if (time > 0) {
					pausing = true;
					zim.animate({target:that, obj:{pausingSpeed:lastSpeed}, ticker:false, time:time, call:function() {
						pausing = false;
						that.speed = lastSpeed;
						that.paused = false;
					}});
				} else {
					pausing = false;
					that.speed = lastSpeed;
					that.paused = false;
				}
			}
			return that;
		}

		Object.defineProperty(that, 'frame', {
			get: function() {
				return _frame;
			},
			set: function(frame) {
				_frame = Math.round(frame) % frames.length;
				var f = frames[_frame];
				if (zot(f)) return;
				sprite.frame = f.f;
			}
		});

		Object.defineProperty(that, 'percentSpeed', {
			get: function() {
				if (that.baseSpeed == 0) return NaN;
				return that.speed / that.baseSpeed * 100;
			},
			set: function(percent) {
				if (pausing || that.paused) return;
				that.speed = that.baseSpeed * percent / 100;
			}
		});

		Object.defineProperty(that, 'pausingSpeed', {
			get: function() {
				if (that.baseSpeed == 0) return NaN;
				return that.speed / that.baseSpeed * 100;
			},
			set: function(percent) {
				that.speed = that.baseSpeed * percent / 100;
			}
		});

		if (style!==false) zimStyleTransforms(this, DS);

		this.dispose = function() {
			cancelAnimationFrame(requestID);
		}
	}
	zim.extend(zim.Dynamo, createjs.EventDispatcher, null, "cjsEventDispatcher");
	//-69.2

/*--
zim.Accelerator = function(objects)

Accelerator
zim class extends a createjs.EventDispatcher

DESCRIPTION
An Accelerator lets you set percentSpeed properties of multiple objects
such as Scroller and Dynamo (Dynamic Sprite) objects
or an object animating with animate() and the dynamic parameter set
All these have a percentSpeed property
which can be set by the Accelerator to operate the speeds.
They can also be paused and paused over time.
An Accelerator object lets you control these from one place

See: https://zimjs.com/explore/sidescroller.html

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var rect = new Rectangle()
	.centerReg()
	.animate({props:{rotation:360}, ease:"linear", dynamic:true});
var accel = new Accelerator(rect);
// adjust speed of the animation by mousedown
// 0% at left, 50% in middle, 100% at right, etc.
new MotionController(accel);
// or
new MotionController({target:accel, type:"mousemove", min:-200, max:200});
// will change speed from -200% to 200% as cursor is moved across screen
END EXAMPLE

EXAMPLE
// assuming we have scroller1, scroller2 and a sprite
// each of these would have a speed set so the scene animates nicely
var accelerator = new Accelerator([scroller1, scroller2, sprite]);

// here we increase the speed then decrease the speed of the whole scene:
animate({target:accelerator, obj:{percentSpeed:200}, time:1000, rewind:true, ticker:false});

// here we change the speed of the whole scene based on the x position of the mouse
// at the very left, the speed is -200 percent and at the right the speed is 200 percent
// in the center, the speed is 0 - damping is optional but always looks better!
// do not rely on stage.mouseX - does not work on touchscreen!
var stageX;
stage.on("stagemousemove", function (e) {stageX = e.stageX;});
var damp = new Damp(accelerator.percentSpeed);
Ticker.add(function() {
	var newSpeed = (stageX-stageW/2)/(stageW/2)*100*2;
	accelerator.percentSpeed = damp.convert(newSpeed);
}, stage);
END EXAMPLE

PARAMETERS
objects - (default null) registers Scroller or Dynamo objects the Accelerator
	pass in a single object or an array of multiple objects

METHODS
add(objects) - registers Scroller or Dynamo objects with the Accelerator
	pass in a single object or an array of multiple objects
	returns the Accelerator object for chaining
remove(objects) - unregisters a Scroller or Dynamo
	pass in a single object or an array of multiple objects
	returns the Accelerator object for chaining
pause(state, time, frameNumber) - pause (default) or unpause all the objects added to the Accelerator
	state - (default true) set to false to unpause the objects added to the Accelerator
	time - (default 0) time in milliseconds to slow down to a speed of 0 and pause
		the pause event and paused property will be set after the time has passed
		time is ignored if a frameNumber is provided
	frameNumber - (default null) get sprites to animate to the frameNumber (probably good for one sprite!)
		setting this will make the scene ignore the time parameter above
dispose() - calls dispose() on all the objects

PROPERTIES
type - holds the class name as a String
percentSpeed - adjusts the speed relative to the baseSpeed of each items in the Accelerator - eg. 100 is at the baseSpeed
	this can be dynamically changed to change all speeds relatively
paused - (read only) whether the Accelerator is paused or not - only tracks if the pause() method is used
items - an array of all objects added with add()
--*///+69.3
	zim.Accelerator = function(objects) {
		z_d("69.3");
		this.cjsEventDispatcher_constructor();

		var that = this;
		this.type = "Accelerator";
		this.paused = false;
		this.items = [];
		this.paused = false;
		this._percentSpeed = 100;
		this.add = function(objects) {
			var list;
			if (Array.isArray(objects)) {list = objects;} else {list = [objects];}
			var ind;
			for (var i=0; i<list.length; i++) {
				ind = that.items.indexOf(list[i]);
				if (ind < 0) that.items.push(list[i]);
			}
			return that;
		}
		if (objects) this.add(objects);
		this.remove = function(objects) {
			var list;
			if (Array.isArray(objects)) {list = objects;} else {list = [objects];}
			var ind;
			for (var i=0; i<list.length; i++) {
				ind = that.items.indexOf(list[i]);
				if (ind >= 0) that.items.splice(ind,1);
			}
			return that;
		}
		this.pause = function(state, time, frameNumber) {
			if (zot(state)) state = true;
			var pausingItems = [];
			if (state) {
				if (!zot(frameNumber)) time = null;
				// if we pause the scene with a time delay or frameNumber
				// then the pause may not happen right away
				// so leave the other animations going like scrollers until the pause
				var waiting = false;
				for (var i=0; i<that.items.length; i++) {
					// if time and not totalFrames and scroller - or - dynamo and (time or frameNumber)
					if ((!zot(time) && zot(frameNumber) && !that.items[i].totalFrames) || that.items[i].totalFrames && (!zot(time) || !zot(frameNumber))) {
						that.items[i].pause(true, time, frameNumber); // frameNumber ignored by scroller
						waiting = true;
						pausingItems[i] = 1;
						that.items[i].on("pause", function(){
							if (!that.paused) {
								pauseAll(true);
								that.paused = true;
								that.dispatchEvent("pause");
							}
						}, null, true);
					}
				}
				// not waiting so pause all
				if (!waiting) {
					pauseAll();
					that.paused = true;
					setTimeout(function() {that.dispatchEvent("pause");}, 10);
				}
			} else {
				that.paused = false;
				pauseAll();
			}
			function pauseAll(fromDelay) {
				for (var i=0; i<that.items.length; i++) {
					// pauseAll does not need to pause the ones we were waiting for and are now done...
					if (pausingItems[i] != 1) {
						that.items[i].pause(state);
					}
				}
			}
		}
		Object.defineProperty(that, 'percentSpeed', {
			get: function() {
				return that._percentSpeed;
			},
			set: function(percent) {
				that._percentSpeed = percent;
				for (var i=0; i<that.items.length; i++) {
					that.items[i].percentSpeed = percent;
				}
			}
		});
		this.dispose = function() {
			for (var i=0; i<that.items.length; i++) {
				that.items[i].dispose();
			}
			return true;
		}
	}
	zim.extend(zim.Accelerator, createjs.EventDispatcher, null, "cjsEventDispatcher");
	//-69.3

/*--
zim.Emitter = function(obj, width, height, interval, num, life, fade, shrink, decayTime, decayStart, trace, traceFadeTime, traceShiftX, traceShiftY, angle, force, gravity, wind, layers, animation, random, horizontal, vertical, sink, sinkForce, cache, events, startPaused, pool, poolMin, particles, style, group, inherit)

Emitter
zim class - extends a zim.Container which extends a createjs.Container

DESCRIPTION
A particle emitter - so this makes and animates display objects like shapes or bitmaps
Particle emitters are often used for things like fireworks, fire, smoke, sparks, falling objects, etc.
The Emitter is filled with options so have a look at the doc parameters
Here are some examples:
https://zimjs.com/particles/

NOTE: consider the Emitter as somewhat experimental and pushing the bounds of the canvas
In future versions we will look into adding CreateJS StageGL (WebGL) examples / support (it might work already)
The Emitter certainly can make excellent and workable effects
But it can also bog the browser if pushed to extremes or sometimes if left going
This possibly means there are memory leaks - we have been doing our best to track things down
The Emitter is reporting an expected number of children so any leaks might be beyond ZIM control

NOTE: each particle starts at the center of the container width and height
If the trace parameter is true then the particle is put in a container that does not move
and the particle moves inside that container as the container is cached with the source-over composite operation
The currentParticle property and all the event objects' particle parameter is the moving particle
However, the children of the Emitter, will be slightly different in each case:
when trace is false, the children of the Emitter container are any active particles
when trace is true, the children of the Emitter container are the containers that hold the active particles
If you have moved, scaled or rotated the Emitter or its container,
then you will want to use var point = myEmitter.localToGlobal(particle.x, particle.y)
and get point.x and point.y to find the location of the particle relative to the stage coordinates

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// eg.1 make a bunch of spewing pink circles affected by gravity
var emitter = new Emitter(new Circle(5, pink))
	.centerReg(stage);

// eg.2 use a sink to attract the particles
var sink = new Circle(10, pink).centerReg(stage).alp(0);
// make one of three types of particles and randomize the colors
var particles = new Emitter({
	obj:[
		new Circle(20,null,darker,2),
		new Rectangle(30,30,null,darker,2),
		new Triangle(40,40,40,null,darker,2)
	],
	random:{color:[blue, green, pink, yellow, orange]},
	interval:20, // default
	life:5000,
	decayTime:1000, // default
	sink:sink,
	sinkForce:.5,
	gravity:0,
	force:1,
	cache:mobile(), // default
})
	.centerReg(stage)
	.sca(2);

// eg. 3 use a StageGL Frame and createjs.SpriteSheetBuilder for circles:
var frame = new Frame({scale:"fit", width:1024, height:768, gpu:true});
frame.on("ready", function() {
	var stage = frame.stage;
	// if we pass in just a Circle then we would have to turn on cache
	// and cache on WebGL counts as an image for each one
	// whereas a SpriteSheet just counts as an image for all of the particles
	// so build a SpriteSheet from the Circle
	var builder = new createjs.SpriteSheetBuilder();
	builder.addFrame(new Circle(50, purple));
	builder.build();
	var emitter = new Emitter({
		obj:new Sprite({spriteSheet:builder.spriteSheet}),
		num:10, // ten Sprites made every 20 ms for about 1000 particles
		life:2000,
		interval:20,
		gravity:0,
		force:2
	}).centerReg(stage);
});


// see more examples at https://zimjs.com/particles
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
obj - |ZIM VEE| (default blue, green, pink Circles) a display object to clone - eg. new Circle(10, green);
	can also specify a shape config object with the following properties to draw inside a shape as an alternative to the trace property
	{type:"shape", s:"white", ss:1, f:"red", sd:{noPick:[20, 10]}, offset:3}
	the parameters accept ZIM VEE values except the type and sd as it requires an array as a final value
	type:"shape" is required.  s is setStroke, ss is setStrokeStyle, sd and offset are setStrokeDash in CreateJS
	line thickness (ss) is currently not staying in the latest CDN CreateJS - this is working in the NEXT build
width - (default 300) the width of the Emitter container - used as cache bounds for trace if trace is true
height - (default 300) the height of the Emitter container - used as cache bounds for trace if trace is true
	these dimensions will affect performance if the trace parameter is true so use carefully
	also see the traceShiftX and traceShiftY to specify the caching rectangle position
interval - |ZIM VEE| (default 300) the time in ms between imitting particles
num - |ZIM VEE| (default 1) the number of particles emitted each interval
life - (default 1000) the time in ms the particle will exist
fade - (default true) Boolean to fade the particle (alpha 0) - set to false to not fade out the particle over the decayTime
shrink - (default true unless trace is true) Boolean to shrink the particle (scale 0) - set to false to not shrink the particle over the decayTime
decayTime - (default 1000) time in ms to fade and / or shrink the particle - ends animation at the life time unless decayStart is set
decayStart - (default null) time in ms to start the decayTime otherwise decay (fade and shrink) ends at the end of life time
trace - (default false) Boolean set to true to leave trails by caching each particle with source-over composite operation
traceFadeTime - (default decayTime) time in ms to fade out traced particle to 0 alpha at the end of the particle life time
traceShiftX - (default 0) x amount to shift the cache rectangle for the traced particle
traceShiftY - (default 0) y amount to shift the cache rectangle for the traced particle
	the particle starts centered in the width and height of the Emitter container
	if you have particles falling - for instance fireworks, you can shift the cache rectangle down to see more trails
	and then place the Emitter up higher on the stage
angle - |ZIM VEE| (default {min:0, max:360}) the angle the particle will emit (0 is along the positive x axis)
	if you want to shoot particles in one direction just use angle = 20
	if you want something shooting up on either side of the y axis you can use:
	angle = {min:-90-20, max:-90+20}; this may be easier to visualize
	if you want to emit at 45 or 90 then use [45, 90]
force - |ZIM VEE| (default 5) the force for the emitter to shoot the partice at an angle
	if you want to shoot a variety use force = {min:2, max:10} etc.
gravity - (default 9.8) the force of gravity going down - can be negative to make particles float up
wind - (default 0) a force you can apply in the horizontal direction either negaitive for left or positive for right
layers - (default "top") where to place the current particle being emitted - values are "top", "bottom", "random"
animation - |ZIM VEE| (default null) a zim animate config object to apply to the particle
	This is the whole zim DUO object to pass to animate - including an props parameter that holds the animation object
random - (default null) an object holding properties to animate, each property holding a ZIM VEE Value object for Pick.choose() to pick from per particle
	eg: {color:["red", "white", "green"], scale:{min:1, max:2}} // scale is a convienence property for both scaleX and scaleY
horizontal - (default false) start the particles across the emitter's width at the top of the emitter (unless vertical is set to true)
vertical - (default false) start the particles across the emitter's height at the left of the emitter (unless horizontal is set to true)
sink - (default null) an object with x and y properties (can be a display object) that the particles will be pulled to (or pushed if sinkForce is negative)
sinkForce - (default 10 if sink) the force particles are moved towards the sink location
cache - (default mobile() or false if gpu) Boolean to cache each particle - helpful if complex shape or text (do not use for Bitmap or SpriteSheet)
events - (default false) Boolean - set to true to receive events from Emitter
startPaused - (default false) Boolean - set to true to start the Emitter in the paused state
pool - (default true) Boolean if true, makes as many particles as it needs before recycling particles
	this improves performance as new particles do not need to be made and old ones remove
	see also the clearPool() method to start collecting a new type of particle, etc.
poolMin - (default 0) a minimum number of pooled particles before new particles are no longer made (if pool is true)
	eg. setting poolMin to 100 would make 100 particles and then start reusing these particles for performance
	if you set pool to true and do not specify a poolMin then ZIM will calculate the needed number to properly recycle
	but you can override this number if you want a larger pool for more selection
particles - (default null) a container for the particles
	by default, the Emitter will make a Container and place it beneath itself when added to the stage.
	by separating the particles from the emitter, it allows the emitter to be moved without all the particles moving

METHODS
spurt(num, time, restart) - shoots particles (usually would pause Emitter before doing this)
	supports ZIM DUO config object
	num - |ZIM VEE| (default null) number of particles to emit according to Emitter settings
	time - |ZIM VEE| (default null) alternatively, time to emit particles according to Emitter settings
		if both num and time are provided the faster one will stop the emitting
		dispatches three different spurt events - see events
	restart (default false) set to true to restart the particles when spurted (removes old particles)
pauseEmitter(state, restart, freeze, immediate) - pause or unpause the Emitter
	state (default true) will pause the emitter or set to false to unpause the emitter
		this will set the read only paused property to true or false accordingly
	restart (default false) set to true to restart the particles when unpaused
		otherwise continues the particles from where they were
	freeze (default false) set to true to freeze the particles
		othewise pause just stops emitting and existing particles continue their life
	immediate (default false) set to true to emit right away after unpausing
		otherwise just emits on normal schedule
clearPool() - clear the pool of particles - use if you change the obj or its properties (no effect if pool parameter is false)
resize(width, height) - resizes the Emitter container and any cache bounds for new particles
hasProp(property as String) - returns true if property exists on object else returns false
clone() - makes a copy with properties such as x, y, etc. also copied
	all current properties will be cloned except for startPaused for which the initial paramter value is cloned
dispose() - removes event listeners from Window and content and removes any Ticker functions

ALSO: ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO: See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
** All the PARAMETERS are available as PROPERTIES to get and set (except for the force and cache parameters - and width and height act differently)
emitterForce - get and set force - do not use force property - due to conflict with physics force
emitterPaused - read only Boolean as to whether the Emitter is paused or not - see also pauseEmitter() method
currentParticle - the latest particle emitted
	if trace is false then this is myEmitter.getChildAt(myEmitter.numChildren-1);
	if trace is true then this is myEmitter.getChildAt(myEmitter.numChildren-1).getChildAt(0);
particlesEmitted - the number of particles that have been made / emitted
spurtNum - total number of particles to spurt (when spurt() is called)
spurtCount - number of particles spurted so far (when spurt() is called)
zimInterval - the interval used to create particles
zimTicker - the ticker used to animate particles
** CHILD PROPERTIES - each child has a particle (if trace is true) or is a particle (if trace is false)
particle -  a reference to the particle for the child (could be to itself)
particleNormal - true or false if particle is not decaying or fizzing
particleDecaying - true or false if decaying - particle is currently animating to either scale 0 or alpha 0
particleFizzing - true or false if fizzing - trace container is currently animating to alpha 0
** setting widths and heights adjusts scale not bounds and getting these uses the bounds dimension times the scale
** these will not remake the cache bounds, etc. - they just scale the Emitter - see resize()
width - gets or sets the width. Setting the width will scale the height to keep proportion (see widthOnly below)
height - gets or sets the height. Setting the height will scale the width to keep proportion (see heightOnly below)
widthOnly - gets or sets the width.  This sets only the width and may change the aspect ratio of the object
heightOnly - gets or sets the height.  This sets only the height and may change the aspect ratio of the object
blendMode - how the object blends with what is underneath - such as "difference", "multiply", etc. same as CreateJS compositeOperation

ALSO: See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
** the below events all have a particle property that gives access to the particle (not the particle container for a traced particle - ask for the particle.parent for that)
dispatches a "spurted" event once the spurt() method is finished emitting particles
dispatches a "spurtdecayed" event once the last spurted particle decays (fade / shrink)
dispatches a "spurtfizzed" event once the last spurted particle's life ends
** the below events only trigger if the events parameter is set to true (default is false for slight performance edge)
dispatches an "emitted" event when a particle is made
dispatches a "decayed" event when the particle's decayStart + decayTime ms has elapsed
dispatches a "fizzed" event when the particle's life ms has elapsed

ALSO: See the CreateJS Easel Docs for Container events, such as:
added, click, dblclick, mousedown, mouseout, mouseover, pressmove, pressup, removed, rollout, rollover
--*///+69.9
	zim.Emitter = function(obj, width, height, interval, num, life, fade, shrink, decayTime, decayStart, trace, traceFadeTime, traceShiftX, traceShiftY, angle, force, gravity, wind, layers, animation, random, horizontal, vertical, sink, sinkForce, cache, events, startPaused, pool, poolMin, particles, style, group, inherit) {
	    var sig = "obj, width, height, interval, num, life, fade, shrink, decayTime, decayStart, trace, traceFadeTime, traceShiftX, traceShiftY, angle, force, gravity, wind, layers, animation, random, horizontal, vertical, sink, sinkForce, cache, events, startPaused, pool, poolMin, particles, style, group, inherit";
	    var duo; if (duo = zob(zim.Emitter, arguments, sig, this)) return duo;
		z_d("69.9");

		this.type = "Emitter";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

	    if (zot(obj)) obj = DS.obj!=null?DS.obj:[new zim.Circle(20,"#e472c4"),new zim.Circle(20,"#acd241"),new zim.Circle(20,"#50c4b7")];
		if (zot(width)) width = DS.width!=null?DS.width:300;
		if (zot(height)) height = DS.height!=null?DS.height:300;
	    if (zot(interval)) interval = DS.interval!=null?DS.interval:20;
	    if (typeof interval == "number") interval = Math.max(10, interval);
		if (zot(num)) num = DS.num!=null?DS.num:1;
		if (zot(trace)) trace = DS.trace!=null?trace.size:false;
		if (zot(traceFadeTime)) traceFadeTime = DS.traceFadeTime!=null?DS.traceFadeTime:decayTime;
		if (zot(traceShiftX)) traceShiftX = DS.traceShiftX!=null?DS.traceShiftX:0;
		if (zot(traceShiftY)) traceShiftY = DS.traceShiftY!=null?DS.traceShiftY:0;
	    if (zot(life)) life = DS.life!=null?DS.life:1000;
	    if (zot(fade)) fade = DS.fade!=null?DS.fade:true;
		if (zot(shrink)) shrink = DS.shrink!=null?DS.shrink:trace?false:true;
	    if (zot(decayTime)) decayTime = DS.decayTime!=null?DS.decayTime:1000;
	    if (zot(angle)) angle = DS.angle!=null?DS.angle:{min:0, max:360};
	    if (zot(force)) force = DS.force!=null?DS.force:5;
	    if (zot(gravity)) gravity = DS.gravity!=null?DS.gravity:9.8
	    if (zot(wind)) wind = DS.wind!=null?DS.wind:0;
	    if (zot(layers)) layers = DS.layers!=null?DS.layers:"top"; // bottom, random
	    if (zot(width)) width = DS.width!=null?DS.width:100;
	    if (zot(height)) height = DS.height!=null?DS.height:100;
	    if (zot(horizontal)) horizontal = DS.horizontal!=null?DS.horizontal:false;
	    if (zot(vertical)) vertical = DS.vertical!=null?DS.vertical:false;
		if (zot(sink)) sink = DS.sink!=null?DS.sink:null;
	    if (!zot(sink) && zot(sinkForce)) sinkForce = DS.sinkForce!=null?DS.sinkForce:10;
		if (zot(events)) events = DS.events!=null?DS.events:false;
		if (zot(startPaused)) startPaused = DS.startPaused!=null?DS.startPaused:false;
		if (zot(pool)) pool = DS.pool!=null?DS.pool:true;
		if (zot(poolMin)) poolMin = DS.poolMin!=null?DS.poolMin:0;
		if (zot(particles)) particles = DS.particles!=null?DS.particles:new zim.Container();
		if (zot(animation)) animation = DS.animation!=null?DS.animation:null;

	    this.zimContainer_constructor(width, height, null, null, false);
		this.type = "Emitter";
	    var that = this;

		this.regX = width/2; // new auto center reg
		this.regY = height/2;

	    // might want many of these as dynamic properties - sigh.
	    that.obj = obj; that.particles = particles;
		that.interval = interval; that.num = num;
	    that.life = life; that.fade = fade; that.shrink = shrink;
		that.decayTime = decayTime;  that.decayStart = decayStart;
		that.trace = trace; that.traceFadeTime = traceFadeTime,
		that.traceShiftX = traceShiftX; that.traceShiftY = traceShiftY;
	    that.angle = angle, that.emitterForce = force;
		that.gravity = gravity; that.wind = wind;
	    that.layers = layers; that.animation = animation; that.random = random;
		that.horizontal = horizontal; that.vertical = vertical;
	    that.sink = sink; that.sinkForce = sinkForce;
		that.events = events; that.startEmitterPaused = startPaused;
		that.pool = pool; that.poolMin = poolMin;

		that.particlesEmitted = 0;
		var poolList = [];
		var poolIndex = 0;
		var poolCount = 0;

		var emitterTicker;
		var stage;
		zim.added(that, addedToStage);
	    function addedToStage(s) {
	        stage = s;
			if (zot(cache)) {
				if (stage.isWebGL) {
					cache = false;
				} else {
					cache = zim.mobile();
				}
			}
			if (cache) stage.snapToPixelEnabled = true;
	        if (stage) {
				var myIndex = that.parent.getChildIndex(that);
				that.parent.addChildAt(particles, myIndex); // add particles beneath emitter in emitter's container
				// particles.x = that.x;
				// particles.y = that.y;
				if (!horizontal && !vertical) that.centerReg(null, null, false);

				//-------------   INTERVAL
				// var counter = 0;
				// var time = Date.now();
				// that.interval = 1000 / 60;

				that.zimInterval = zim.interval(that.interval, function() {
	            // that.zimInterval = setInterval(function() {

					// counter++;
				    // zog(decimals(counter/(Date.now() - time)*1000,5));

					if (that.startEmitterPaused) {that.pauseEmitter(); return;}
					// want to leave that.obj as it was provided
					// but for creation we will normalize it as an Array
					obj = Array.isArray(that.obj)?that.obj:[that.obj];
	                if (obj.length <= 0) return;

					var minInterval; // used to calculate max pool
					var maxNum;
					function sortNumber(a,b) {return a - b;}
					if (Array.isArray(that.interval)) {
						that.interval.sort(sortNumber);
						minInterval = that.interval[0];
					} else if (that.interval.constructor == {}.constructor){
						minInterval = that.interval.min;
					} else {
						minInterval = that.interval;
					}
					if (Array.isArray(that.num)) {
						that.num.sort(sortNumber);
						maxNum = that.num[that.num.length-1];
					} else if (that.num.constructor == {}.constructor){
						maxNum = that.num.max;
					} else {
						maxNum = that.num;
					}

					zim.loop(zim.Pick.choose(that.num), function() {
		                if (that.decayTime > 0) {
		                    var decay = {};
		                    if (that.shrink) decay.scale=0;
		                    if (that.fade) decay.alpha=0;
		                }
						if (that.pool && poolList.length > 0 && poolCount >= Math.max(that.poolMin, (that.life/minInterval+5)*maxNum)) { // USE POOL... throw in an extra 5 for good measure
							var container = poolList[(poolIndex++)%poolList.length];
							container.visible = true;
							var particle = container.trace?container.getChildAt(0):container;
							if (particle.emitShape) {
								var t = particle.template;
								particle.graphics.c().s(t.s?zim.Pick.choose(t.s):null).ss(t.ss?zim.Pick.choose(t.ss):null).sd(t.sd?zim.Pick.choose(t.sd):null, t.offset?zim.Pick.choose(t.offset):null);
							}
							if (container.trace) container.updateCache();
							if (that.layers == "top") {
								if (particle.emitShape) container.addTo(particles, null, false);
								else container.centerReg(particles);
							} else {
								if (particle.emitShape) container.addTo(particles, that.layers=="bottom"?0:zim.rand(particles.numChildren), false);
								else container.centerReg(particles, that.layers=="bottom"?0:zim.rand(particles.numChildren));
							}
							container.alpha = 1;
							container.scaleX = 1;
							container.scaleY = 1;
							particle.alpha = particle.originalAlpha;
							particle.scaleX = particle.originalScaleX;
							particle.scaleY = particle.originalScaleY;
							particle.endSpurt = false;

						} else { // END POOL, START NOT POOL
							poolCount++;
							var container;
							if (that.trace) {
								container = new zim.Container(width,height, null, null, false);
								container.trace = true;
							}

							var template = zim.Pick.choose(zim.shuffle(obj)[0]);
							if (template.type == "shape") { // shape needs to be top left whereas others need to be centerReg
								var t = template;
								// {type:"shape", s:"white", ss:2, f:null, sd:[20, 10], offset:3}
								// line thickness is currently not staying in the latest CDN CreateJS - this is working in the NEXT build
								var particle = new zim.Shape(1,1,null,null,null,false);
								particle.emitShape = true;
								particle.template = t;
								particle.graphics.s(t.s?zim.Pick.choose(t.s):null).ss(t.ss?zim.Pick.choose(t.ss):null).sd(t.sd?zim.Pick.choose(t.sd):null, t.offset?zim.Pick.choose(t.offset):null);
								if (that.trace) {
									particle.addTo(container, null, false)
								} else {
									if (that.layers == "top") {
										particle.addTo(particles, null, false);
									} else {
										particle.addTo(particles, that.layers=="bottom"?0:zim.rand(particles.numChildren), null, false);
									}
								}
							} else { // others need to be centerReg
								var particle = template.clone();
								if (!particle.centerReg) zimify(particle);
								if (that.trace) {
									particle.centerReg(container).pos({x:-1000,y:-1000,reg:true}); // cache was drawing this in center - perhaps missing an update so just move it away
								} else {
									if (that.layers == "top") {
										particle.centerReg(particles);
									} else {
										particle.centerReg(particles, that.layers=="bottom"?0:zim.rand(particles.numChildren));
									}
								}
							}

							if (that.trace) { // still need to add the container if there is one
								if (that.layers == "top") {
									container.addTo(particles, null, false);
								} else {
									container.addTo(particles, that.layers=="bottom"?0:zim.rand(particles.numChildren), false);
								}
								container.cache(traceShiftX,traceShiftY,width,height);
							}

							if (!that.trace) container = particle;
							container.particle = particle;
							particle.originalAlpha = particle.alpha;
							particle.originalScaleX = particle.scaleX;
							particle.originalScaleY = particle.scaleY;

						} // END NOT POOL

						that.currentParticle = particle;
						that.particlesEmitted++;

						container.particleNormal = true;
						container.particleDecaying = false;
						container.particleFizzing = false;

						var angle = zim.Pick.choose(that.angle);
		                var speed = zim.Pick.choose(that.emitterForce);
		                var speedX = speed*Math.cos(angle*Math.PI/180);
		                var speedY = speed*Math.sin(angle*Math.PI/180);
		                particle.info = {
		                    position: {x:that.x, y:that.y},
		                    velocity: {x:speedX, y:speedY}
		                }
						if (that.horizontal) particle.info.position = {x:that.x-that.regX+zim.rand(0, width), y:that.y-that.regY+(that.vertical?height/2:0)};
		                if (that.vertical) particle.info.position = {x:that.x-that.regX+(that.horizontal?width/2:0), y:that.y-that.regY+zim.rand(0, height)};
						if (particle.emitShape) {
							particle.graphics.mt(particle.info.position.x, particle.info.position.y);
						} else {
							particle.x = particle.info.position.x,
							particle.y = particle.info.position.y;
						}

						if (that.random) {
		                    // {color:[blue, pink, yellow, orange]}
		                    zim.loop(that.random, function(property, options) {
		                        val = zim.Pick.choose(options);
		                        if (property == "scale") {
		                            particle.sca(val);
		                        } else {
									if (property == "x") {
										particle.info.position.x = (that.horizontal || that.vertical)?val:val+width/2;
									} else if (property == "y") {
										particle.info.position.y = (that.horizontal || that.vertical)?val:val+height/2;
									}
									if ((property == "x" || property == "y") && particle.emitShape) {
										particle.graphics.mt(particle.info.position.x, particle.info.position.y);
									}
		                            particle[property] = val;
									if (particle.emitShape) {particle.x=0; particle.y = 0;} // just adjust info for x and y of shape
		                        }
		                    });
		                }

				        if (cache && !particle.emitShape) particle.cache(particle.getBounds().x-10,particle.getBounds().y-10,particle.getBounds().width+20,particle.getBounds().height+20);

						//-------------    DECAY, FADE, SHRINK, FIZZ

						// do not shrink shapes as registration is at 0,0 not at the particle
						var shrinkMe = particle.emitShape?false:that.shrink;

		                if (that.decayTime > 0 && (that.fade || shrinkMe || (that.trace && that.traceFadeTime > 0))) {


							if (that.trace && that.traceFadeTime > 0) {
								container.animate({
									obj:{alpha:0},
									time:that.traceFadeTime,
									wait:that.life-that.traceFadeTime,
									waitedCall:function(t) {
										t.particleNormal = false;
										t.particleFizzing = true;
									},
									call:fizz,
									override:false,
									id:"decay"
								});
							}
							if (that.fade || shrinkMe) {
								var o = {};
								if (that.fade) o.alpha = 0;
								if (shrinkMe) {
									o.scaleX = 0;
									o.scaleY = 0;
								}
			                    particle.animate({
									obj:o,
									time:that.decayTime,
									wait:zot(that.decayStart)?that.life-that.decayTime:that.decayStart,
									waitedCall:function(t) {
										if (t.parent != that) t = t.parent; // access container if there is one
										t.particleNormal = false;
										t.particleDecaying = true;
									},
									call:function(t) {
										if (that.events) sendEvent("decayed", t);
										if (t.endSpurt) sendEvent("spurtdecayed", t);
										if (!(that.trace && that.traceFadeTime > 0)) {
											if (zot(that.decayStart) || that.decayStart+that.decayTime>that.life) {
												if (t.parent) fizz(t.parent.trace?t.parent:t); // only want to call fizz once
											} else {
												-function() {
						                            var c = container;
						                            zim.timeout(that.life-(that.decayStart+that.decayTime), function(){fizz(c);})
						                        }();
											}
										}
									},
									override:false,
									id:"decay"
								});
							}
		                } else {
		                    if (that.life > 0) {
		                        -function() {
		                            var c = container;
		                            c.timeOut = zim.timeout(that.life, function(){fizz(c);})
		                        }();
		                    }
		               	}
						if (that.events) sendEvent("emitted", container);
						spurtCheck(particle);
		                if (that.animation) {
							var a = zim.Pick.choose(that.animation);
		                    if (zot(a.override)) a.override = false;
		                    particle.animate(zim.copy(a));
		                }
					});
				// }, that.interval);
	            }, null, true); // true for immediate


				//-------------  TICKER

				var framerate = 0; // need a Ticker before can get the framerate so set this after Ticker
				emitterTicker = that.emitterTicker = zim.Ticker.add(function() {
	                zim.loop(particles, function(particle) {
						if (particle.trace) {
							var particleContainer = particle;
							particle = particle.getChildAt(0);
						}
	                    var p = particle.info;
	                    var sinkX = 0;
	                    var sinkY = 0;
	                    // will add multiple sinks in the future
	                    // also - try sink with negative force to deflect
	                    if (!zot(that.sink)) {
	                        var b = that.particles.localToGlobal(p.position.x, p.position.y);
							var s;
							if (that.sink.parent && that.sink.parent.localToGlobal) {
								var s = that.sink.parent.localToGlobal(that.sink.x, that.sink.y);
							} else {
								s = new createjs.Point(zim.Pick.choose(that.sink.x), zim.Pick.choose(that.sink.y));
							}
	                        var sinkAngle = zim.angle(b.x, b.y, s.x, s.y);
	                        var sinkX = that.sinkForce*Math.cos(sinkAngle*Math.PI/180)
	                        var sinkY = that.sinkForce*Math.sin(sinkAngle*Math.PI/180)
	                    }
	                    var ax = that.wind + sinkX;
	                    var ay = that.gravity + sinkY;
	                    p.velocity.x += ax*frameRate;
	                    p.velocity.y += ay*frameRate;
						p.position.x += p.velocity.x*frameRate*100;
	                   	p.position.y += p.velocity.y*frameRate*100;
						if (particle.emitShape) {
							particle.graphics.lt(p.position.x, p.position.y);
						} else {
		                    particle.x = p.position.x;
		                    particle.y = p.position.y;
						}
						if (that.trace && particleContainer) particleContainer.updateCache(particle.emitShape?null:"source-over");

					});
	            }, stage);
	            frameRate = 1 / zim.Ticker.framerate;
	        }
	    };

		//-------------    FUNCTIONS

		function fizz(p) {

			if (that.events) sendEvent("fizzed", p);
			if (p.trace?p.getChildAt(0).endSpurt:p.endSpurt) {
				sendEvent("spurtfizzed", p);
				that.spurting = false;
			}
			if (that.pool) {
				if (p.pooled == "end") {
					// already removed from poolList by clearPool
					// go through and removeChild
				} else if (p.pooled) { // already in the pool
					p.visible = false;
					return;
				} else { // add to pool and return so does not get removed
					p.pooled = true;
					poolList.push(p);
					p.visible = false;
					return;
				}
			}
			that.removeChild(p);
			if (that.trace) p.uncache();
			p = null;
		}

		function sendEvent(type, object) {
			var e = new createjs.Event(type);
			e.particle = object.trace?object.getChildAt(0):object;
			that.dispatchEvent(e);
		}

		function spurtCheck(p) {
			if (zot(that.spurtCount) && zot(that.spurtNum)) return;
			that.spurtCount++;
			if (that.spurtCount >= that.spurtNum) {
				lastSpurt(p);
			}
		}

		function lastSpurt(p) {
			that.pauseEmitter();
			that.spurtCount = that.spurtNum = null;
			sendEvent("spurted", p);
			p.endSpurt = true;

		}

		//-------------    PROPERTIES

		Object.defineProperty(that, 'interval', {
			get: function() {
				return interval;
			},
			set: function(theInterval) {
				interval = theInterval;
				if (that.zimInterval) that.zimInterval.time = interval;
			}
		});

		//-------------    METHODS

		this.spurting = false;
		this.spurt = function(num, time, restart) {
			var sig = "num, time, restart";
	   	 	var duo; if (duo = zob(that.spurt, arguments, sig)) return duo;

			if (!zot(time)) {
				zim.timeout(zim.Pick.choose(time), function() {
					lastSpurt(that.currentParticle);
				});
				that.spurting = true;
			}
			if (!zot(num)) {
				// handled by checkSpurt() function
				that.spurtNum = zim.Pick.choose(num);
				that.spurtCount = 0;
				that.spurting = true;
			}
			that.pauseEmitter(false, restart, null, true); // unpause and immediately call call interval function
			return that;
		}

		this.clearPool = function() {
			zim.loop(that, function(p) {
				p.pooled="end";
				if (!p.visible) that.removeChild(p);
			},true);
			poolCount = 0;
			poolIndex = 0;
			poolList = [];
		}

		if (!that.startEmitterPaused) this.emitterPaused = false; // do not set to false - as it will be done below if needed
		this.pauseEmitter = function(state, restart, freeze, immediate) {
			that.startEmitterPaused = null;
			if (zot(state)) state = true;
			if (zot(restart)) restart = false;
			if (zot(freeze)) freeze = false;
			if (state) { // pausing
				if (that.emitterPaused) return that;
				if (freeze) {
					if (emitterTicker) zim.Ticker.remove(emitterTicker);
			        zim.loop(that, function(particle) {
			            particle.pauseAnimate();
						if (particle.trace) particle.getChildAt(0).pauseAnimate();
						if (particle.timeOut) {
							particle.timeOut.pause();
						}
			        });
				}
		        that.zimInterval.pause();
				that.emitterPaused = true;
			} else { // unpausing
				if (!that.emitterPaused) return that;
				if (restart) {
					zim.loop(that, function(particle) {
			            particle.stopAnimate();
						if (particle.timeOut) particle.timeOut.clear();
						if (particle.trace) particle.getChildAt(0).pauseAnimate();
			        });
					that.removeAllChildren();
				}
				if (stage && emitterTicker && !zim.Ticker.has(stage, emitterTicker)) {
					zim.Ticker.add(emitterTicker, stage);
			        zim.loop(that, function(particle) {
			            particle.pauseAnimate(false);
						if (particle.timeOut) particle.timeOut.pause(false);
						if (particle.trace) particle.getChildAt(0).pauseAnimate(false);
			        });
				}
		        that.zimInterval.pause(false, immediate);
				that.emitterPaused = false;
			}
			return that;
		}

		this.resize = function(w, h) {
			if (!zot(w)) width = w;
			if (!zot(h)) height = h;
			that.setBounds(0,0,width,height);
			if (!horizontal && !vertical) that.centerReg();
			that.clearPool();
		}

		if (style!==false) zimStyleTransforms(this, DS);

	    this.clone = function() {
			var objClone;
			if (Array.isArray(that.obj) || that.obj.constructor == {}.constructor) {
				objClone = zim.copy(that.obj);
			} else if (that.obj.clone) {
				objClone = that.obj.clone();
			} else {
				objClone = that.obj;
			}
			// note that all will clone the current property except for startEmitterPaused which clones the initial parameter value
			return that.cloneProps(new zim.Emitter(objClone, width, height, that.interval, that.num, that.life, that.fade, that.shrink, that.decayTime, that.decayStart, that.trace, that.traceFadeTime, that.traceShiftX, that.traceShiftY, that.angle, that.emitterForce, that.gravity, that.wind, that.layers, that.animation, zim.copy(that.random), that.horizontal, that.vertical, that.sink, that.sinkForce, cache, that.events, startPaused, that.pool, that.poolMin));
	    }

	    this.dispose = function() {
	        if (emitterTicker) {
				zim.Ticker.remove(emitterTicker);
			}
	        zim.loop(that, function(particle) {
	            particle.stopAnimate();
	        });
	        if (that.zimInterval) that.zimInterval.clear();
	        return true;
	    }
	}
	zim.extend(zim.Emitter, zim.Container, "clone", "zimContainer", false);
	//-69.9

/*--
zim.Pen = function(size, color, penType, damp, spread, borderColor, borderWidth, end, paper, nib, cache, ctrlKey, cropScale, undo, undoKeys, draggable, onTop, deleteable, doubleClickDelete, immediateStop, lineAlpha, lineBlendMode, style, group, inherit)

Pen
zim class - extends a ZIM Container which extends a CreateJS Container

DESCRIPTION
Handles dynamic drawing with a set of different penTypes
You can drag() or gesture() pen, move it with a MotionController(),
animate() it, animate() it along a Squiggle or Blob path, etc.

Holding down the CTRL key will allow the pen to move without drawing

SEE: https://zimjs.com/pen.html

NOTE: the pen adds either a Bitmap (if cache is true) or a Shape (if cache is false)
to the container the pen is added to.  This is layered directly under the pen.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var pen = new Pen({penType:"kitetail", nib:new Circle(10,pink)}).center().drag();
END EXAMPLE

EXAMPLE
// use a MotionController
var pen = new Pen().center();
var motionController = new MotionController({
	target:pen,
	type:"pressmove",
	speed:60,
	damp:.5,
	mouseMoveOutside:true
});

// if later, animation along a path is desired
// the motionController needs to be disabled
// and the pen set to write
motionController.enabled = false;
pen.write = true;

// position the pen at the start of the path
// and set its damping to star there too...
pen.loc(path.pointControls[0]);
pen.immediate(pen.x, pen.y);

// if animating along a path more than once
// make sure the percentComplete is reset to 0
// (this is a tricky one!)
pen.animate({
	set:{percentComplete:0},
	props:{path:new Squiggle().center()},
	time:1000,
	call:function () {
		// to use the motionController again:
		pen.write = false;
		motionController.enabled = true;
	}
})
END EXAMPLE

PARAMETERS
** supports DUO - parameters or single object with properties below
** supports VEE - parameters marked with ZIM VEE mean a zim Pick() object or Pick Literal can be passed
   Pick Literal formats: [1,3,2] - random; {min:10, max:20} - range; series(1,2,3) - order, function(){return result;} - function
** supports OCT - parameter defaults can be set with STYLE control (like CSS)
size - |ZIM VEE| (default depends on penType) the pen size
color - |ZIM VEE| (default depends on penType) the pen size
damp - (default .1) the damping for the pen - 0 is no movement, 1 is no damping
	usually, set to 1 or false for following animate paths
penType - (default "line") a String of "line","kitetail","barbwire","grass","hair","city", or "splatter"
spread - |ZIM VEE| (default depends on penType) some pens use this like the splatter to determine how much the circles spread
borderColor - |ZIM VEE| (default depends on penType) a line down the middle of the pen drawing
borderWidth - |ZIM VEE| (default depends on penType) the thickness of a line down the middle of the pen drawing
end - (default "butt") the cap type as a String "butt", "square", "round" - from CreateJS
paper - (default null) a ZIM Container to hold the drawing - or Pen will make a Container to use
	see also the paper property to change containers - for layers in a drawing for instance
nib - (default null) an optional DisplayObject that will be used as the pen - would suggest centerReg({add:false}) this
cache - (default true) caches drawing in a Bitmap (improves performance) - set to false to not cache - the paper property points to the Bitmap or the Shape depending
ctrlKey - (default true) turns off drawing when CTRL key is being pressed. Set to false to not turn off drawing when the CTRL key is pressed
cropScale - (default 1) number times stage dimensions image will be cropped
	if dragging or shifting shape and paper, may want to set to 3 times the stage size for instance
	NOTE: keep this at 1 for iOS to avoid oversize canvas issues.  Set to cropScale:mobile()=="ios"?1:3
undo - (default 0) number of undo levels for example 30
	undo will be automatically recorded when each drawing stops
	undo will also be recorded if dragging a segment(s) with SHIFT or CTRL SHIFT or deleting a segment with delete()
undoKeys - (default true - if undo is > 0) use CTRL Z for undo and CTRL Y or CTRL SHIFT Z for redo - set to false to handle manually
draggable - (default true) set to false to not be able to drag segments or SHIFT drag all
onTop - (default true) - when dragging, does the selected line come to the top in the current paper
deleteable - (default true) - set to false to not be able to delete segments with CTRL click or all segments with CTRL SHIFT click
doubleClickDelete - (default true) - set to false to not use dblclick to delete segment
immediateStop - (default true) - if pressup is used (drag or MotionController) then stop drawing immediately when press is up
	set to false to keep drawing until damping is finished.
lineAlpha - (default 1) the alpha for each line drawn
lineBlendMode - (default "normal") the blendMode for each line drawn () - such as "difference", "multiply", etc. same as CreateJS compositeOperation

METHODS
setPen(newPen) - sets the pen and resets the default properties for the pen
immediate(x, y) - set the pen to this location without damping
clear() - clears the drawing
stop() - stops drawing and saves segment - may start drawing right away again if pen is moving...
	see infinite property to make sure drawing continues as one segment until stop() is called
	infinite is automatically set to true when animating pen along a path
	and stop() is called when animating along a path is complete (or paused)
saveState(obj, startLayer, endLayer) - record an undo state for the paper or a line segment if it is transformed - for an undo()
	use after manually resizing, positioning, rotating, setting alpha and skewing the paper or a line segment
	saveState() is not needed for dragging a line segment with SHIFT, CTRL SHIFT or if dragState or dragAllState is true
	startLayer and endLayer are for if the layer level of the shape changes
undo() - go back one undo state (called automatically by CTRL Z if undoKeys is true - default)
redo() - go forward one undo state (called automatically by CTRL Y or CTRL SHIFT Z if undoKeys is true - default)
delete(index) - delete a line segment at a given index (actually sets its alpha to 0 to maintain layers on undo)
	use: pen.paper.on("mousedown", function (e) {
		pen.delete(paper.getChildIndex(e.target)); // for instance
	})
deleteSegment(object) - delete a line segment object
	use: pen.paper.on("mousedown", function (e) {
		pen.deleteSegment(e.target); // for instance
	})
dispose(paper) - removes all listeners and clears the drawing
	if  the optional paper is provided the specified paper will be disposed - not the pen

ALSO ZIM 4TH adds all the methods listed under Container (see above), such as:
drag(), hitTestRect(), animate(), sca(), reg(), mov(), center(), centerReg(),
addTo(), removeFrom(), loop(), outline(), place(), pos(), alp(), rot(), setMask(), etc.
ALSO See the CreateJS Easel Docs for Container methods, such as:
on(), off(), getBounds(), setBounds(), cache(), uncache(), updateCache(), dispatchEvent(),
addChild(), removeChild(), addChildAt(), getChildAt(), contains(), removeAllChildren(), etc.

PROPERTIES
type - holds the class name as a String
paper - the Container that holds the drawing which are Bitmap objects if cache is true or Shape objects if cache is false
	the paper is centered on the stage and may be bigger or smaller depending on the cropScale setting
	the paper can be changed to a different Container or swapped back and forth for layer type control
	paper has shape, bitmap and paperNum properties
	paper can be specifically disposed() with thePen.dispose(thePaper)
	this should only be used as a way to dispose a series of individual papers other than the current paper
	use thePen.dispose() to dispose of the pen and the current paper
lastSegment - gets the last segment drawn - for instance use in a stop event function to get the line last drawn
	this will be a Bitmap (if cache is true) or a Shape (if cache is false)
	all segments have a paper property referring to its paper (which is also the segment's parent property)
lastSelected - gets the last selected segment - perhaps a segment was dragged, etc.
nib - get a reference to the specified nib - often not used
write - get or set whether the pen is writing to the paper
drawing - read only Boolean as to if the pen is drawing as in moving
size - |ZIM VEE| get or set the size of the pen
sizeFactor - (default 1) get or set a factor to multiply the size of the pen (after being picked from ZIM VEE)
	works independent of sizeScale
sizeScale - (default 1) get or set scaling on the size of the pen (after being picked from ZIM VEE)
	works independent of sizeScale
spread - |ZIM VEE| get or set the spread of the pen
spreadFactor - (default 1) get or set a factor to multiply the spread of the pen (after being picked from ZIM VEE)
	works independent of spreadScale
spreadScale - (default 1) get or set scaling on the spread of the pen (after being picked from ZIM VEE)
	works independent of spreadFactor
color - |ZIM VEE| get or set the color of the pen
borderColor - |ZIM VEE| get or set the borderColor of the pen
borderWidth - |ZIM VEE| get or set the borderWidth of the pen
penType - get or set the type of pen - this will NOT set default properties like setPen()
undoLevels - get or set the number of undo levels
undoKeys - get or set if CTRL Z and CTRL Y / CTRL SHIFT Z are used for undo and redo
draggable - read-only if pen is draggable
	to make pen not draggable set pen.paper.mouseEnabled = false;
immediateStop - get or set how drawing segments ends - "both", "pressup", "mousemove" and "either" are options (see parameter for more info)
infinite - boolean to keep recording until stop() is called

ALSO See the CreateJS Easel Docs for Container properties, such as:
x, y, rotation, scaleX, scaleY, regX, regY, skewX, skewY,
alpha, cursor, shadow, mouseChildren, mouseEnabled, parent, numChildren, etc.

EVENTS
dispatches a "drawing" event when drawing - note lastSegment is assigned when the drawing is stopped
dispatches a "stop" event when pen motion stops
	If the immediateStop parameter is "both" (default) then the pen must also be "up" to trigger the stop
	This is if drag() or motionController({type:"pressmove"}) is being used.
	If the immediateStop parameter is "mousemove" then the stop will trigger when the motion stops - regardless of the pen being up or down
	This setting may cause small drawing segments at corners of lines or if the pen is held down and moved slowly
	If the immediateStop is either then either the pressup or mousemove stopping - which ever comes first - will end segment
dispatches a "change" event for when the drawing has a new segment (or on undo / redo)
dispatches a "paperChange" event when undo or redo changes paper objects
	The pen switches papers just before the paperChange is dispatched but before the change event is dispatched
dispatches a "paperMove" event when the paper moves with CTRL drag
dispatches a "recordUndo" when any type of undo is recorded - new segment, delete, drag, clear
--*///+69.93
	zim.Pen = function(size, color, penType, damp, spread, borderColor, borderWidth, end, paper, nib, cache, ctrlKey, cropScale, undo, undoKeys, draggable, onTop, deleteable, doubleClickDelete, immediateStop, lineAlpha, lineBlendMode, style, group, inherit) {
		var sig = "size, color, penType, damp, spread, borderColor, borderWidth, end, paper, nib, cache, ctrlKey, cropScale, undo, undoKeys, draggable, onTop, deleteable, doubleClickDelete, immediateStop, lineAlpha, lineBlendMode, style, group, inherit";
		var duo; if (duo = zob(zim.Pen, arguments, sig, this)) return duo;
		z_d("69.93");
		this.zimContainer_constructor();
		this.type = "Pen";
		this.group = group;
		var DS = style===false?{}:zim.getStyle(this.type, this.group, inherit);

		if (zot(penType)) penType = DS.penType!=null?DS.penType:"line";
		var base = {
			size:2,
			color:"#333",
			spread:{min:5,max:20},
			borderColor:"#111",
			borderWidth:0,
		}
		var defaults = {
			line:{size:2},
			kitetail:{
				size:{min:5, max:20},
				color:zim.series(zim.pink,zim.blue,zim.green),
				borderColor:"rgba(0,0,0,.5)",
				borderWidth:1
			},
			grass:{
				color:"#acd241",
				size:3,
				spread:{min:10, max:30}
			},
			hair:{
				color:["#e472c4", "#50c4b7"],
				size:3,
				spread:{min:20, max:50}
			},
			city:{
				size:{min:30, max:70},
				spread:{min:50, max:200},
				color:["#333","#111","#555"],
			},
			barbwire:{},
			splatter:{
				size:{min:5, max:20},
				color:"rgba(0,0,0,.5)"
			}
		}
		if (zot(defaults[penType])) penType = "line";

		if (zot(size)) size = DS.size!=null?DS.size:zot(defaults[penType].size)?base.size:defaults[penType].size;
		if (zot(color)) color = DS.color!=null?DS.color:zot(defaults[penType].color)?base.color:defaults[penType].color;
		if (zot(damp)) damp = DS.damp!=null?DS.damp:.1;
		if (damp === false) damp = 1;
		if (zot(spread)) spread = DS.spread!=null?DS.spread:zot(defaults[penType].spread)?base.spread:defaults[penType].spread;
		if (zot(borderColor)) borderColor = DS.borderColor!=null?DS.borderColor:zot(defaults[penType].borderColor)?base.borderColor:defaults[penType].borderColor;
		if (zot(borderWidth)) borderWidth = DS.borderWidth!=null?DS.borderWidth:zot(defaults[penType].borderWidth)?base.borderWidth:defaults[penType].borderWidth;
		if (zot(end)) end = DS.end!=null?DS.end:"butt"; // also round and square
		if (zot(cropScale)) cropScale = DS.cropScale!=null?DS.cropScale:1;
		if (zot(cache)) cache = DS.cache!=null?DS.cache:true;
		if (zot(undo)) undo = DS.undo!=null?DS.undo:0;
		if (zot(undoKeys)) undoKeys = DS.undoKeys!=null?DS.undoKeys:true;
		if (zot(draggable)) draggable = DS.draggable!=null?DS.draggable:true;
		if (zot(deleteable)) deleteable = DS.deleteable!=null?DS.deleteable:true;
		if (zot(doubleClickDelete)) doubleClickDelete = DS.doubleClickDelete!=null?DS.doubleClickDelete:true;
		if (zot(onTop)) onTop = DS.onTop!=null?DS.onTop:true;
		if (zot(immediateStop)) immediateStop = DS.immediateStop!=null?DS.immediateStop:"both";
		if (zot(lineAlpha)) lineAlpha = DS.lineAlpha!=null?DS.lineAlpha:1;
		if (zot(lineBlendMode)) lineBlendMode = DS.lineBlendMode!=null?DS.lineBlendMode:"normal";

		if (zot(paper)) paper = DS.paper!=null?DS.paper:new zim.Container();
		if (zot(nib)) nib = DS.nib!=null?DS.nib.clone():null;

		var that = this;
		this.dampX = new zim.Damp(null, damp);
		this.dampY = new zim.Damp(null, damp);
		var _draw = true;
		this.drawing = false;
		this.immediateStop = immediateStop;
		this.undoLevels = undo;
		this.undoKeys = undoKeys;
		this.draggable = draggable;
		this.lineAlpha = lineAlpha;
		this.lineBlendMode = lineBlendMode;
		var shape = new zim.Shape();
		var w = zimDefaultFrame?zimDefaultFrame.stage.width:1024;
		var h = zimDefaultFrame?zimDefaultFrame.stage.height:768;
		var startXCheck;
		var startYCheck;
		var startLayer;
		var endLayer;
		var draggingCheck = false;
		that.paperNum = 0;
		that.sizeScale = 1;
		that.spreadScale = 1;
		that.sizeFactor = 1;
		that.spreadFactor = 1;
		that.stop = function() {};

		shape.addTo(paper);

		this.added(function () {
			that.paper = paper;
			if (!zot(nib)) {
				if (that.zimDown) {
					that.nib = nib.addTo(that)
				} else {
					// nib not added to that as that is placed by motionController probably...
					// and the two damping systems fight one another
					// so place pen at cursor - except when dragging
					that.nib = nib.addTo(that.parent, that.parent.getChildIndex(that));
					that.nib.x = that.x;
					that.nib.y = that.y;
					that.nib.mouseEnabled = false;
					var stage;
					that.stage.on("stagemousedown", function(e) {
						stage = e.target.stage;
						// moveNib(e); // might want to do this but then would move if dragging existing
						that.nibEvent = that.stage.on("stagemousemove", moveNib);
					});
					that.stage.on("stagemouseup", function(e) {
						that.stage.off("stagemousemove", that.nibEvent);
					});
					function moveNib(e) {
						if (!that.drawing) return;
						that.nib.x = e.stageX/stage.scaleX;
						that.nib.y = e.stageY/stage.scaleY;
					}
				}
			}
			that.zimDragCheck = false;

			that.stop = function() {that.stopCheck(true);}
			that.infinite = false;
			that.stopCheck = function(override) {

				if (!override) {
					if (that.infinite) return;
					if (that.zimDragCheck) return;
					if (!that.immediateStop && that.drawing) return;
				} else { // override - stop for sure!
					that.drawing = false;
					that.infinite = false;
				}
				that.immediate(that.x, that.y);

				setTimeout(function(){
					if (that.drawing) return;
					if (cache) {
						shape.cache(-(w*cropScale-w)/2, -(h*cropScale-h)/2, w*cropScale, h*cropScale);
						var line = paper.bitmap = new zim.Bitmap(shape.cacheCanvas).reg((w*cropScale-w)/2, (h*cropScale-h)/2).addTo(paper);
						shape.graphics.clear();
						shape.uncache();
						shape.top();
					} else {
						var line = shape;
					}
					if (that.undoLevels > 0) {
						var data = {paper:paper, line:line};
						undo.push(data);
						if (undo.length > that.undoLevels) undo.unshift(); // take off front
						that.dispatchEvent("recordUndo");
					}
					if (!cache)  shape = new zim.Shape().clone().addTo(paper);
					that.lastSegment = line;
					that.lastSelected = line;
					line.alpha = that.lineAlpha;
					line.blendMode = that.lineBlendMode;
					line.paper = paper;
					that.dispatchEvent("stop");
					that.dispatchEvent("change");
				}, (that.immediateStop||override?0:50));
			}

			var count = 0;
			that.dampX.immediate(that.x);
			that.dampY.immediate(that.y);
			that.lastX = that.x;
			that.lastY = that.y;
			that.ticker = zim.Ticker.add(function () {

				var newX = that.dampX.convert(_draw?that.x:that.finishX);
				var newY = that.dampY.convert(_draw?that.y:that.finishY);
				var newPoint = that.parent.localToLocal(newX, newY, shape);
				var pickedSize = zim.Pick.choose(size)*that.sizeScale*that.sizeFactor;
				var pickedSpread = zim.Pick.choose(spread)*that.spreadScale*that.spreadFactor;

				if (Math.abs(that.lastX-newX)+Math.abs(that.lastY-newY)<1) {
					if (that.drawing) {
						that.drawing = false;
						if (that.zimDown) { // being dragged
							that.x = that.lastX = _draw?that.x:that.finishX;
							that.y = that.lastY = _draw?that.y:that.finishY;
							that.dampX.immediate(that.x);
							that.dampY.immediate(that.y);
						} else {
							that.x = that.finishX = that.lastX = newX;
							that.y = that.finishY = that.lastY = newY;
						}
						that.stopCheck();
					}
					that.lastX = newX;
					that.lastY = newY;
					return;
				} else {
					if (!that.drawing) {
						// drawing starting again
						redo = [];
						if (paper.getChildIndex(shape) != paper.numChildren-1) shape.top();
					}
					that.drawing = true;
					that.dispatchEvent("drawing");
				}

				if (penType == "splatter") {
					for (var i=0; i<=3; i++) {
						var angle = zim.rand(360)*Math.PI/180;
						var d = pickedSpread;
						var loc = {x:newX+d*Math.cos(angle), y:newY+d*Math.sin(angle)}
						var point = that.parent.localToLocal(loc.x,loc.y, shape);
						shape.graphics.mt(point.x, point.y).f(zim.Pick.choose(color)).dc(point.x, point.y, pickedSize/2)
					}
				} else if (penType == "grass" || penType == "hair" || penType == "city") {
					if (penType == "grass" || penType == "hair") { // add more
						var midX = that.lastX+(newX-that.lastX)/2;
						var midY = that.lastY+(newY-that.lastY)/2;
						var midPoint = that.parent.localToLocal(midX,midY, shape);
						shape.graphics
							.s(zim.Pick.choose(color)).ss(pickedSize, end)
							.mt(midPoint.x, midPoint.y)
							.lt(midPoint.x+rand(-pickedSpread/4, pickedSpread/4), penType=="hair"?midPoint.y+pickedSpread:midPoint.y-pickedSpread);
					}
					if (penType == "grass" || penType == "hair" || (penType == "city" && count%3==0)) {
						shape.graphics
							.s(zim.Pick.choose(color)).ss(pickedSize, end)
							.mt(newPoint.x, newPoint.y)
							.lt(newPoint.x+(penType=="city"?0:rand(-pickedSpread/4, pickedSpread/4)), penType=="hair"?midPoint.y+pickedSpread:newPoint.y-pickedSpread);
					}
					count++;
				} else {
					if (penType == "kitetail") {
						shape.graphics.s(zim.Pick.choose(color)).ss(pickedSize, end)
					}
					if (penType == "barbwire") {
						shape.graphics.s(zim.Pick.choose(color)).ss(pickedSize, end)
						var midX = that.lastX+(newX-that.lastX)/2+rand(-pickedSpread, pickedSpread);
						var midY = that.lastY+(newY-that.lastY)/2+rand(-pickedSpread, pickedSpread);
					} else {
						var midX = that.lastX+(newX-that.lastX)/2;
						var midY = that.lastY+(newY-that.lastY)/2;
					}
					var midPoint = that.parent.localToLocal(midX,midY, shape);
					shape.graphics.qt(midPoint.x, midPoint.y, newPoint.x, newPoint.y);
				}

				var lastPoint = that.parent.localToLocal(that.lastX, that.lastY, shape);
				if (borderWidth>0 && penType != "line") {
					var midX = that.lastX+(newX-that.lastX)/2;
					var midY = that.lastY+(newY-that.lastY)/2;
					var midPoint = that.parent.localToLocal(midX,midY, shape);
					shape.graphics.s(zim.Pick.choose(borderColor)).ss(zim.Pick.choose(borderWidth)).mt(lastPoint.x, lastPoint.y).qt(midPoint.x, midPoint.y, newPoint.x, newPoint.y);
					if (penType == "splatter") shape.graphics.es();
				}

				if (penType == "splatter") {
					shape.graphics.f(zim.Pick.choose(color));
				} else if (penType == "kitetail") {
				} else {
					shape.graphics.s(zim.Pick.choose(color)).ss(pickedSize, end);
				}
				if (penType != "grass" && penType != "hair" && penType != "city" && penType != "splatter") {
					shape.graphics
						.mt(lastPoint.x, lastPoint.y)
						.qt(midPoint.x, midPoint.y, newPoint.x, newPoint.y);
				}
				that.lastX = newX;
				that.lastY = newY;
			});

			that.stage.on("stagemouseup", function () {
				draggingCheck = false; // make sure as cleared paper will not trigger pressup - sigh
			});

		});
		that.mouseChildren = false;

		// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// UNDO REDO FUNCTIONALITY
		// undo data: {paper:Container, paperTransform:{}, line:Bitmap/Shape, lineTransform:{}}

		var undo = [];
		var redo = [];

		this.saveState = function(obj, startLayer, endLayer) { // called by dragging paper or line segment
			var t = obj.type=="Container"?"paper":"line";
			var data = {paper:that.paper};
			data[t] = obj;
			data[t+"Transform"] = {x:obj.x, y:obj.y, r:obj.rotation, a:obj.alpha, rX:obj.regX, rY:obj.regY, sX:obj.scaleX, sY:obj.scaleY, skX:obj.skewX, skY:obj.skewY, v:obj.visible};
			if (!zot(startLayer) && !zot(endLayer) && startLayer != endLayer) {
				data.startLayer = startLayer;
				data.endLayer = endLayer;
			}
			undo.push(data);
			that.dispatchEvent("recordUndo");
			if (undo.length > that.undoLevels) undo.unshift(); // take off front
		}
		var defaultTransform = {x:0, y:0, r:0, a:1, rX:0, rY:0, sX:1, sY:1, skX:0, skY:0, v:true};
		this.undo = function() {
			var data = undo.pop();
			if (data) {
				redo.push(data);
				if (data.clear) {
					for (var i=0; i<data.clear.length; i++) {
						data.paper.addChild(data.clear[i]);
					}
					data.paper.addChild(shape); // add back to top
				} else if (data.line && data.lineTransform) { // undo line transform
					if (!zot(data.startLayer)) { // watch - could be 0
						shape.top();
						data.paper.setChildIndex(data.line, startLayer);
					}
					undoObjTransform("line", data.line);
				} else if (data.line) { // undo add
					data.line.removeFrom();
				} else if (data.paperTransform) { // undo paper transform
					undoObjTransform("paper", data.paper);
				}
				that.lastPaper = that.paper;
				if (data.paper != that.paper) {
					that.paper = data.paper;
					that.dispatchEvent("paperChange");
				}
				that.dispatchEvent("change");
				if (!OPTIMIZE && that.stage) that.stage.update();
			}
		}
		function undoObjTransform(type, obj) {
			var d;
			for (var i=undo.length-1; i>=0; i--) {
				if (undo[i][type]==obj && undo[i][type+"Transform"]) {
					d = undo[i][type+"Transform"];
					break;
				}
			}
			if (!d) d = zim.copy(defaultTransform);
			obj.x=d.x; obj.y=d.y; obj.alpha=d.a; obj.rotation=d.r; obj.regX=d.rX; obj.regY=d.rY; obj.scaleX=d.sX; obj.scaleY=d.sY; obj.skewX=d.skX; obj.skewY=d.skY; obj.visible=d.v;
			if (type=="line" && cache) { // if cached, the cropScale adjusts the starting registration of the Bitmap
				obj.regX = (w*cropScale-w)/2;
				obj.regY = (h*cropScale-h)/2
			}
		}

		this.redo = function() {
			var data = redo.pop();
			var obj;
			if (data) {
				undo.push(data);
				if (data.clear) {
					data.paper.removeAllChildren();
					data.paper.addChild(shape);
				} else if (data.line && data.lineTransform) { // redo line transform
					if (!zot(data.endLayer)) { // watch - could be 0
						shape.top(); // may not matter if paper is not current paper as it will not have shape
						data.paper.setChildIndex(data.line, endLayer); // watch - data.paper not paper
					}
					var d = data.lineTransform;
					var obj = data.line;
				} else if (data.line) { // redo add
					data.line.addTo(data.paper);
				} else if (data.paperTransform) { // redo paper transform
					var d = data.paperTransform;
					var obj = data.paper;
				}
				if (obj) { // keep brackets as multiple statements - I keep trying to put on one line!
					obj.x=d.x; obj.y=d.y; obj.alpha=d.a; obj.rotation=d.r; obj.regX=d.rX; obj.regY=d.rY; obj.scaleX=d.sX; obj.scaleY=d.sY; obj.skewX=d.skX; obj.skewY=d.skY; obj.visible=d.v;
				}
				that.lastPaper = that.paper;
				if (data.paper != that.paper) {
					that.paper = data.paper;
					that.dispatchEvent("paperChange");
				}
				that.dispatchEvent("change");
				if (!OPTIMIZE && that.stage) that.stage.update();
			}
		}

		that.ctrlKey = false;
		that.shiftKey = false;
		that.ctrlKeyCheck = false;
		that.zimkeydownEvent = window.addEventListener("keydown", function(e) {
			that.ctrlKey = e.ctrlKey;
			that.shiftKey = e.shiftKey;
			if (e.keyCode == 17 && draggable && !that.ctrlKeyCheck) { // ctrl key
				that.ctrlKeyCheck = true;
				paper.noDrag();
				paper.drag({onTop:onTop, all:true});
			}
			if (that.undoLevels <= 0) return;
			if (!that.undoKeys) return;
	        if (e.ctrlKey && ((e.shiftKey && e.keyCode == 90) || e.keyCode == 89)) {
	            that.redo();
	        } else if (e.ctrlKey && e.keyCode == 90) {
	            that.undo();
	        }
		});
		that.zimkeyupEvent = window.addEventListener("keyup", function(e) {
			if (e.keyCode == 17 && draggable && that.ctrlKeyCheck) { // ctrl key
				that.ctrlKeyCheck = false;
				paper.noDrag();
				paper.drag({onTop:onTop});
			}
			if (!e.ctrlKey) that.ctrlKey = false;
			if (!e.shiftKey) that.shiftKey = false;
		});

		Object.defineProperty(this, 'paper', {
			get: function() {
				return paper;
			},
			set: function(value) {
				if (value.type != "Container") return;
				paper = value;
				shape.addTo(paper, 0, false); // do not match shape in old container as paper might be moved
				paper.shape = shape;
				that.dampX.immediate(that.x);
				that.dampY.immediate(that.y);
				that.lastX = that.finishX = that.x;
				that.lastY = that.finishY = that.y;
				if (penType == "barbwire") {
					shape.graphics.s(zim.Pick.choose(color)).ss(zim.Pick.choose(size)*that.sizeScale*that.sizeFactor, end);
				}
				shape.graphics.mt(that.x, that.y);

				if (!paper.parent) paper.addTo(that.parent, that.parent.getChildIndex(that));
				if (zot(paper.paperNum)) {
					// apply paper events for delete and drag
					paper.paperNum = ++that.paperNum;
					if (deleteable) {
						paper.on("mousedown", function (e) {
							that.lastSelected = e.target;
							if (that.nibEvent) that.stage.off("stagemousemove", that.nibEvent); // do not place nib if dragging existing
							var currentPaper = e.target.paper;
							if (!currentPaper) return;
							if (currentPaper != that.paper) {
								that.paper = currentPaper;
								that.dispatchEvent("paperChange");
							}
							if (that.shiftKey) {
								if (that.ctrlKey) {
									that.clear(); // clear will dispatch change
								} else {
									// e.target.alpha = 0;
									e.target.visible = false;
									if (that.undoLevels > 0) that.saveState(e.target);
									that.dispatchEvent("change");
								}
							}
						});
					}
					if (doubleClickDelete) {
						paper.on("dblclick", function(e) {
							// e.target.alpha = 0;
							e.target.visible = false;
							if (that.undoLevels > 0) that.saveState(e.target);
						});
					}
					if (draggable) {
						paper.on("mousedown", function (e) {
							stage = e.target.stage;
							draggingCheck = true; // used so motioncontroller knows to ignore in stopCheck
							if (that.shiftKey && deleteable) return;
							startXCheck = e.stageX/stage.scaleX;
							startYCheck = e.stageY/stage.scaleY;
							shape.top();
							startLayer = paper.getChildIndex(e.target);
						});
						paper.drag({onTop:onTop});
						paper.on("pressup", function (e) {
							if (deleteable && that.shiftKey) return;
							// if (e.target.alpha == 0) return;
							if (e.target.visible == false) return;
							if (that.undoLevels <= 0) return;
							if (Math.abs(e.stageX/stage.scaleX-startXCheck)<1 && Math.abs(e.stageY/stage.scaleY-startYCheck)<1) {
								if (onTop) {
									shape.top();
									endLayer = paper.getChildIndex(e.target);
									if (startLayer != endLayer) that.saveState(e.target, startLayer, endLayer);
								}
								return;
							}
							if (that.ctrlKey) {
								that.saveState(paper);
								that.dispatchEvent("paperChange");
							} else {
								shape.top();
								endLayer = paper.getChildIndex(e.target);
								that.saveState(e.target, startLayer, endLayer);
							}
							that.dispatchEvent("change");
							shape.top();
						});
					}
				}
			}
		});

		Object.defineProperty(this, 'write', {
			get: function() {
				return _draw;
			},
			set: function(value) {
				if (_draw && value===false) {
					that.finishX = that.x;
					that.finishY = that.y; // let damping finish
				}
				if (!_draw && value) {
					shape.graphics.es();
                    shape.graphics.ef();
					that.dampX.immediate(that.x);
					that.dampY.immediate(that.y);
					that.lastX = that.x;
					that.lastY = that.y;
				}
				_draw = value;
			}
		});

		Object.defineProperty(this, 'size', {
			get: function() {
				return size;
			},
			set: function(value) {
				shape.graphics.es();
				shape.graphics.ef();
				size = value;
				if (penType != "splatter") shape.graphics.ss(zim.Pick.choose(size)*that.sizeScale*that.sizeFactor, end);
			}
		});

		Object.defineProperty(this, 'color', {
			get: function() {
				return color;
			},
			set: function(value) {
				color = value;
				shape.graphics.es();
				shape.graphics.ef();
				if (penType != "splatter") shape.graphics.s(zim.Pick.choose(color));
			}
		});
		Object.defineProperty(this, 'penType', {
			get: function() {
				return penType;
			},
			set: function(value) {
				penType = value;
				shape.graphics.ef().es();
				if (penType != "splatter") {
					shape.graphics.s(zim.Pick.choose(color));
				} else {
					shape.graphics.s(zim.Pick.choose(color)).ss(zim.Pick.choose(size)*that.sizeScale*that.sizeFactor, end);
				}
			}
		});

		Object.defineProperty(this, 'damp', {
			get: function() {
				return damp;
			},
			set: function(value) {
				damp = value;
				that.dampX.damp = damp;
				that.dampY.damp = damp;
			}
		});

		Object.defineProperty(this, 'spread', {
			get: function() {
				return spread;
			},
			set: function(value) {
				spread = value;
			}
		});

		var startColor;
		var endColor;
		this.setColorRange = function(color1, color2) {
			if (zot(color2)) {
				startColor = that.color;
				endColor = color1;
			} else if (zot(color1)) {
				startColor = that.color;
				endColor = color2;
			} else {
				startColor = color1;
				endColor = color2;
			}
			return that;
		}
		var _colorRange = 0;
		Object.defineProperty(that, 'colorRange', {
			get: function() {
				return _colorRange;
			},
			set: function(value) {
				_colorRange = value;
				if (!zot(startColor) && !zot(endColor)) {
					that.color = zim.colorRange(startColor, endColor, value);
				}
			}
		});

		Object.defineProperty(this, 'borderColor', {
			get: function() {
				return borderColor;
			},
			set: function(value) {
				borderColor = value;
			}
		});

		Object.defineProperty(this, 'borderWidth', {
			get: function() {
				return borderWidth;
			},
			set: function(value) {
				borderWidth = value;
			}
		});

		if (zimDefaultFrame) {
			zimDefaultFrame.on("keydown", function () {
				if (frame.ctrlKey && that.write) {
					that.lastWrite = that.write;
					that.write = false;
				}
			});
			zimDefaultFrame.on("keyup", function () {
				if (!frame.ctrlKey && that.write == false) {
					that.write = that.lastWrite;
				}
			});
		}

		that.setPen = function(newPen) {
			that.dampX.immediate(that.x);
			that.dampY.immediate(that.y);
			that.lastX = that.finishX = that.x;
			that.lastY = that.finishY = that.y;
			if (zot(newPen)) newPen = penType;
			if (zot(defaults[newPen])) newPen = "line";
			that.penType = penType = newPen;
			var newDefaults = zim.merge(base, defaults[newPen]);
			for (var prop in newDefaults) {
				if (that[prop] != newDefaults[prop]) {
					that[prop] = newDefaults[prop];
				}
			}
			return that;
		}

		that.immediate = function(x, y) {
			if (!zot(x)) {
				that.x = x;
				that.dampX.immediate(that.x);
				that.lastX = that.finishX = that.x;
			}
			if (!zot(y)) {
				that.y = y;
				that.dampY.immediate(that.y);
				that.lastY = that.finishY = that.y;
			}
			return that;
		}

		that.clear = function() {
			if (paper.numChildren <= 1) return; // shape does not count
			var data = []; var obj;
			for (var i=0; i<paper.numChildren-1; i++) {
				data.push(paper.getChildAt(i))
			}
			undo.push({paper:paper, clear:data});
			if (undo.length > that.undoLevels) undo.unshift(); // take off front
			that.dispatchEvent("recordUndo");
			paper.removeAllChildren();
			shape.graphics.clear();
			paper.addChild(shape);
			that.dispatchEvent("change");
			if (that.stage) that.stage.update();
			return that;
		}

		this.delete = function(index) {
			// paper.getChildAt(index).alpha = 0;
			paper.getChildAt(index).visible = false;
			if (that.undoLevels > 0) that.saveState(paper.getChildAt(index));
		}
		this.deleteSegment = function(segment) {
			// segment.alpha = 0;
			segment.visible = false;
			if (that.undoLevels > 0) that.saveState(segment);
		}

		that.dispose = function(paper) {
			if (!zot(paper)) {
				paper.removeAllEventListeners();
				paper.shape.graphics.clear();
				if (cache) paper.bitmap.removeFrom();
				else paper.shape.removeFrom();
				return;
			}
			window.removeEventListener("keydown", that.zimkeydownEvent);
			window.removeEventListener("keyup", that.zimkeyupEvent);
			Ticker.remove(that.Ticker);
			that.removeAllEventListeners();
			paper.removeAllEventListeners();
			shape.graphics.clear();
			if (cache) that.bitmap.removeFrom();
			else shape.removeFrom();
			nib.removeFrom();
			return true;
		}

		if (style!==false) zimStyleTransforms(this, DS);
	};
	zim.extend(zim.Pen, zim.Container, "clone", "zimContainer", false);
	//-69.93

/*--
zim.SoundWave = function(num, input, include, smoothing, min, max, operation, baseline, magnify, reduce)

SoundWave
zim class - extends a CreateJS EventDispatcher

DESCRIPTION
Receives a sound input and calculates frequency data using HTML AudioContext createAnalyser()
The input can be the mic or the result of a asset("someSound").play() or an <audio> tag
You can specify the number of data points and then use the calculate() method to animate to sound

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var soundWave = new SoundWave(50, "mic");
soundWave.on("ready", function() {
	Ticker.add(function() {
		var data = soundWave.calculate();
		// data is an array with 50 frequency amplitudes from low to high based on Microphone input
	})
});

// or pass in a sound instance:

// before loading the sound with frame.loadAssets() use the following:
// this forces CreateJS to use an <audio> tag format
// we are trying to get things to work with WebAudio and will remove this message when we do!
createjs.Sound.registerPlugins([createjs.HTMLAudioPlugin]);

// later when we the loading is complete:
var soundWave = new SoundWave(50, frame.asset("mySound.mp3").play());

// or pass in an <audio> tag reference:
var soundWave = new SoundWave(50, zid("tagID"));
zid("tagID").play();

// see more examples at
// https://zimjs.com/soundwave/bars.html
// https://zimjs.com/soundwave/circles.html
// https://zimjs.com/soundwave/mouth.html
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
num - (default 120) Number of data points returned by the calculate() method
input - (default "mic") or can set to the results of a asset("someSound").play()
	or can set to an <audio> tag reference zid("tagID") make sure to zid("tagID").play()
include - (default 120/1024 = .117) a decimal range to include (0-1) - the full range (1) includes 90% very high frequencies
smoothing - (default .85) a decimal range for smoothing with 0 being choppy and .9 being slow to respond, etc.
min - (default -80 mic -100 song) minimum decibel number to pick up
max - (default -40 mic -10 song) maximum decibel number to pick up
operation - (default function below) a function that is applied to each result in the original bufferLength (1024)
	the natural results are very bass heavy with roughly a straight line heading down as frequency gets higher
	the default function reduces the bass by half and slowly rises towards the original values for higher frequency
		function(amplitude, i) {
			return amplitude * (.5+i*1/Math.pow(SoundWave.bufferLength, .95));
		})
	you can pass in a different function to take the place of the default function
	the function receives the original amplitude and index as parameters
	you can use SoundWave.bufferLength to get the total number of values in the original data (1024)
	Note: the data returned by the calculate() method will be only the included range - eg. .117 of the total original values (starting at low frequency)
baseline - (default 0 for mic and 30 for sound) removes this amount of amplitude from each data point (after operation is applied)
magnify - (default 1 for mic and 10 for sound) multiplies the data point by this much (after the baseline is removed)
	by removing the baseline amount and multiplying what's left the difference in wave data is increased
reduce - (default 0) subtracts this amount from each data point (after magnified)

METHODS
calculate() - returns an array of amplitudes at various frequencies from low to high
	the array will have a length that matches the num parameter
	the range of frequencies used will be 1024 multiplied by the include factor - eg. .117 = 120
	this 120 will be divided by the num parameter and average results over the range will be used
	this means the num parameter must be less than the 1024 times the range otherwise there is a warning

PROPERTIES
type - holds the class name as a String
num - read only num of frequency data
smoothing - a decimal range for smoothing with 0 being choppy and .9 being slow to respond, etc.
analyser - the HTML analyser object https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode
	with minDecibels, maxDecibels, smoothingTimeConstant and some others - see link
baseline - removes this amount of amplitude from each data point (after operation is applied)
magnify - multiplies the data point by this much (after the baseline is removed)
reduce - subtracts this amount from each data point (after magnified)

EVENTS
dispatches a "ready" event when the sound source is connected and the calculate() method is ready
--*///+69.95
	zim.SoundWave = function(num, input, include, smoothing, min, max, operation, baseline, magnify, reduce) {
		var sig = "num, input, include, smoothing, min, max, operation, baseline, magnify, reduce";
		var duo; if (duo = zob(zim.SoundWave, arguments, sig, this)) return duo;
		z_d("69.95");
		this.type = "SoundWave";
		if (zot(num)) num = 120;
		if (zot(input)) input = "mic";
		if (zot(include)) include = 120/1024;
		if (zot(smoothing)) smoothing = .85;
		if (zot(min)) min = input=="mic"?-80:-100;
		if (zot(max)) max = input=="mic"?-40:-10;
		if (zot(operation)) operation = function(amplitude, i) {
			return amplitude * (.5+i*1/Math.pow(zim.SoundWave.bufferLength, .95));
		}
		if (zot(baseline)) baseline = (input=="mic"?0:30); // subtracts this much from value
		if (zot(magnify)) magnify = (input=="mic"?1:10); // multiplies amount by this much
		if (zot(reduce)) reduce = 0; // after calculating, subtract this much

		zim.SoundWave.bufferLength = 1024;
		_num = num;
		var that = this;
		that.baseline = baseline;
		that.magnify = magnify;
		that.reduce = reduce;

		var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
		var analyser = that.analyser = audioCtx.createAnalyser();
		analyser.minDecibels = min;
		analyser.maxDecibels = max;
		analyser.smoothingTimeConstant = smoothing;

		Object.defineProperty(this, 'smoothing', {
			get: function() {
				return analyser.smoothingTimeConstant;
			},
			set: function(s) {
				analyser.smoothingTimeConstant = s;
			}
		});

		Object.defineProperty(this, 'num', {
			get: function() {
				return _num;
			},
			set: function(n) {
				_num = n;
				steps = Math.floor(include*zim.SoundWave.bufferLength / _num);
				if (steps < 1) zog("ZIM SoundWave: num is too big");
			}
		});

		if (input == "mic") {
			navigator.getUserMedia = (navigator.getUserMedia ||
									  navigator.webkitGetUserMedia ||
									  navigator.mozGetUserMedia ||
									  navigator.msGetUserMedia);

			if (navigator.getUserMedia) {
				navigator.getUserMedia (
					{audio: true},
					function(stream) {
						var source = that.source = audioCtx.createMediaStreamSource(stream);
						connectSource(source);
					},
					function(err) {
						zog("ZIM SoundWave: Error occured: " + err);
					}
				);
			} else {
				zog("ZIM SoundWave: Sorry, mic not supported");
			}
			return;
		} else {
			if (input.type && input.type == "sound") {zog("ZIM SoundWave: pass in the result of a zim.asset('somesound').play() for the input"); return;}
			if (input.playbackResource) {
				// zog(input.playbackResource)
				// var source = audioCtx.createBufferSource(input.playbackResource);
				// source.connect(audioCtx.destination);
				// audioCtx.decodeAudioData(
				// 	input.playbackResource,
				// 	function(buffer) {
				// 		source.buffer = buffer;
				// 		source.connect(audioCtx.destination);
				// 		source.loop = true;
				// 	},
				// 	function(e){console.log("Error with decoding audio data" + e.err);}
				// );
				// var source = that.source = audioCtx.createBufferSource(input.playbackResource);
				var audio = input.playbackResource; // a playing zim.asset("somesound").play()
				var source = audioCtx.createMediaElementSource(audio);
			// } else if (input.buffer) {
			// 	zog("here")
			// 	var source = input;
			// 	zog(source.connect)
			} else {
				var audio = input; // a playing <audio> tag zid("soundTagID").play()
				var source = audioCtx.createMediaElementSource(audio);
			}
			connectSource(source)
		}
		var steps;
		function connectSource(source) {
			source.connect(analyser);
			if (input != "mic") analyser.connect(audioCtx.destination);

			analyser.fftSize = zim.SoundWave.bufferLength*2;
			steps = Math.floor(include*zim.SoundWave.bufferLength / _num);
			if (steps < 1) {zog("ZIM SoundWave: include param is too small or num param is too big"); return;}
			var bufferLength = analyser.frequencyBinCount;
			var dataArray = new Uint8Array(bufferLength);

			that.calculate = function() {
				analyser.getByteFrequencyData(dataArray);
				var adjustedArray = dataArray.map(operation);
				if (steps == 1) return adjustedArray;
				var array = [];
				var tot = 0;
				for (var i=0; i<=include*zim.SoundWave.bufferLength; i++) {
					tot += adjustedArray[i];
					if (i==0) continue;
					if (i%steps==0) {
						array.push(Math.max(0,(tot/steps-that.baseline)*that.magnify-that.reduce));
						tot = 0;
					}
				}
				// array.push((tot/steps-30)*10);
				if (input != "mic") {
					array[0] *= .75;
					array[1] *= .85;
					array[2] *= .9;
					array[array.length-2] *= .8;
					array[array.length-1] *= .75;
				}
				array[array.length-1] *= 1.3;
				array[array.length-2] *= 1.2;
				array[array.length-3] *= 1.1;
				return array;
			}
			setTimeout(function(){that.dispatchEvent("ready");}, 50);
		}

	}

	zim.extend(zim.SoundWave, createjs.EventDispatcher, null, "cjsEventDispatcher", false);

//-69.95

/*--
zim.VR = function(content, angle, distance, parallax, parallaxAngle, damp, parallaxDamp, startAngle, negativeParallax, borderMarkers, swiper, holder)

VR
zim class - extends a ZIM Container which extends a CreateJS Container

DESCRIPTION
Copies the content and displays a left and right channel (side-by-side) to be viewed with a VR helmet.
Content can be made to shift side to side as the head turns.  Parallax is also an option.
Works with Google Cardboard & Daydream, Samsung Gear VR, Carl Zeiss VR, Merge VR, View-Master VR
And costing around $20-$30 these are fine: Utopia 360, VRIT VR, ETVR, etc.
Provides a ZIM Swiper for testing on desktop to emulate turning the head.

DETAILS
Make a content Container (with registration point at 0,0) that holds all the content.
Set a depth property on any children in the content container that you want to look 3D
or use the chainable dep() method to apply the depth property.
ZIM VR will copy the content and its depths into left and right containers and mask it.
The content can be accessed via contentLeft and contentRight properties.
The x values of the content that has depth properties set will be shifted according to the depth.
This gives a sense of 3D coming out of the screen for positive depths and into the screen for negative depths.

Device orientation can also be used to move the content as the head is turned and the headset and device moves.
Currently the horizontal movement is built in as that is the trickiest with the 3D effect.
Supply a motion angle (angle) and a motion distance (distance) about which the user will turn their head to see the content.
Optional arrows are provided to map the boundaries of rotation and available as borderLeft and borderRight properties.
You can manually apply vertical shift based on the ZIM Frame deviceOrientation event about the x axis

Horizontal Parallax is provided if desired and will be based on the head rotation if no motion angle is provided
or based on the distance of the object from the center of view if a motion angle is provided.
You can set the parallax details as well like the magnitude, angle, damping, and negative parallax.
Negative values of depth will not produce parallax unless negativeParallax is set to true (this looks funny).
You can make your own vertical parallax if desired using ZIM Parallax and the deviceOrientation event about the x axis.

NOTE: moving content in the x direction should not be done manually.
Rather, use the position() method of VR to properly place both channels and reset the vrStartX positions required.

NOTE: To add or remove content please use the register() and remove() methods.
Any new content should be added to the initial content container.
Do this first, then set the desired depths, then register the new content.
This will clone the new content to the right side channel
and set the shift in x based on depths for both channels.

NOTE: Any modifications afterwards to other properties like alpha, color, y, registration, etc.
must be made manually to both channels - use the contentLeft and contentRight properties.
The contentLeft is the original content (also available as the content property).
You can use the vrMatch property of any content objects to get its other channel partner.
So if you have a label variable for the Label in the original content,
you can access the label in the right hand channel with label.vrMatch
The right channel objects also have vrMatch properties so label.vrMatch.vrMatch is the label ;-)

SEE: https://zimjs.com/vr/

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// Prepare content inside one container
var content = new Container(stageW/2, stageH);

// This background tile has no depth or is at "screen" depth
// There will be no depth shift and no parallax
new Tile(new Rectangle(10,10,light), 20, 1, null, 10).center(content);

// These rectangles are given depth using the dep() method - or set the depth property
// The rectangles will be shifted in the x by VR
// centerReg the objects for proper parallax
var box = new Rectangle(50,50,green).centerReg(content).dep(10);
new Rectangle(70,70,clear,blue,5).centerReg(content).dep(12);

// 1. this just shows content in 3D with no motion:
// var vr = new VR(content).addTo(stage);

// 2. this will move when the head is turned (device is rotated) but with no parallax:
// var vr = new VR(content, 160, 400).center(stage);

// 3. this shows parallax when the head is turned but with no positional movement:
// var vr = new VR(content, 0, 400, 2, 60).center(stage);

// 4. this shows motion and parallax as the head is turned:
var vr = new VR(content, 160, 400, 2, 60).center(stage);


// this will change the color of the box after it has been added to VR:
// box.color = pink; // the left side
// box.vrMatch.color = pink; // the right side

// this would fade in and out the outer rectangles:
// var obj = {obj:{alpha:0}, loop:true, rewind:true};
// vr.contentLeft.getChildAt(2).animate(obj);
// vr.contentRight.getChildAt(2).animate(obj);

// this would remove the outer rectangle:
// vr.remove(vr.content.getChildAt(2));

// this would remove the right hand border marker
// which shows when angle is != 0 and borderMarkers is set to true (default)
// vr.remove(vr.borderRight);

// this shows an inter-ocular adjuster when the stage is pressed
stage.on("stagemousedown", function() {
	vr.showAdjuster();
});

// the above works fine, but really should remove the event when stage is pressed
// because all presses using the adjuster would be calling showAdjuster
// then add the event again when the adjuster is closed
// but ZIM makes sure that adjuster does not re-open if it is already open.
// Here is what that code would look like:

// var adjusterEvent = stage.on("stagemousedown", function() {
// 	vr.showAdjuster();
// }, null, true); // run once
// vr.on("close", function() {
// 	adjusterEvent = stage.on("stagemousedown", adjusterEvent);
// });

// this code turns the stage yellow if device is rotated beyond the angle parameter value
var adjusterEvent = vr.on("boundaryout", function(e) {
	zog(e.boundary); // would be "left" or "right"
	frame.color = yellow;
});

// this code changes the stage color back to the original when rotated back in bounds of the angle parameter
vr.on("boundaryin", function(e) {
	zog(e.boundary); // would be "left" or "right"
	frame.color = lighter;
});

// this would load stereoscopic images to the left and right content (no need to registser)
// frame.loadAssets(["stairsRight.jpg", "stairsLeft.jpg"]);
// frame.on("complete", function() {
// 	frame.asset("stairsLeft.jpg").center(vr.contentLeft);
// 	frame.asset("stairsRight.jpg").center(vr.contentRight);
// });
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
content - a ZIM Container with registration point at the top left that holds the content
	The content will be placed in the left VR Channel (Container) and cloned to the right VR Channel (Container)
	Set the depth property (or use dep()) of any children (recursive) of the content to be viewed in 3D
	The higher the depth, the more it comes out of the screen which is at 0 depth
	A negative depth will go into the screen but will not have parallax (see negative parallax)
	The left and right content are available as contentLeft and contentRight properties of VR
	Each child of content has a vrMatch property that refers to itself in the other channel
angle - (default 0) the angle in degrees the head can turn to see VR move (half to left, half to right)
	Currently, there is a small glitch in the transition at 360 degrees as damping rewinds and reverses position
	And there is no affordance for wrapping - so content at this location is not recommended
	A fix for this might be provided some time in the future
distance - (default 100) the pixels the content will move over the angle (half to left, half to right)
	OR if there is no angle and there is parallax, the distance the parallax will move over the parallaxAngle
parallax - (default 0) the percentage of the depth it will move for each degree of motion up to the parallaxAngle
parallaxAngle - (default 60) the degree range that parallax will operate (half on one side and half on the other)
damp - (default .5) the damping for motion if angle is provide - quicker than normal for more response turning
parallaxDamp - (default .1) the damping for parallax - smaller number goes slower towards desired position
startAngle - (default 0) the angle to start the viewing
negativeParallax - (default false) set to true to make parallax work for negative depth (looks funny)
boundaryMarkers - (default true) set to false to remove arrow markers at angle boundaries if angle is set
swiper - (default false) set to true to use swiping if not on mobile device - for testing
	If on mobile, swiper is set to false so it does not conflict with the rotation of the device
	The swiper works from -330 to +330 and then stops
	Currently, there is no wrapping system so the limits prevent a glitchy transition
holder - (default zimCurrentFrame stage) set to a Container with bounds set if desired - usually, the stage (default)

METHODS
register(item) - content is usually passed in initially to the content parameter of VR
	but an item (a display object) can be added at any time - to remove items, see remove().
	Set the desired depth property for the item with 0 being the default for the screen depth.
	Add the item to content (contentLeft) with addTo(), center(), centerReg() before registering.
	Do not add the item to contentRight as register() will clone the new item and its children recursively
	to the right side VR Channel and set its depth and parallax.
	NOTE: to add stereoscopic images, one to the left chanel and one to the right channel,
	just add the images using addTo(), center() or centerReg() to contentLeft and contentRight - no need to register()
	returns the item
remove(item) - removes item (and children) from the content of VR - do not manually remove from left and right channels
	returns the item
position(item, x, y) - positions item in VR in both left and right channels - do no manually position left and right
	returns the item
showAdjuster() - shows a pannel to set interocular distance - this is saved across app loads
	The pannel container can be accessed with adjuster property of VR
	The adjuster property has backing, label, strip, slider, close, zero and ok properties for formatting
	The adjuster sets the regX property of the content to shift the two channels to the desired distance appart
	Generally, this should be the distance between the eyes, but experimentation shows that a smaller distance works well
	This is provided primarily for tablet or desktop where the distance may be too big for the cross-eyed viewing technique
	returns the VR object
hideAdjuster() - hides the adjuster - but the close and ok buttons will do this as well - also see VR events...
	returns the VR object
resize() - call the resize() method to resize VR if the holder size changes - VR can be added to the ResizeManager
	returns the VR object

PROPERTIES
type - holds the class name as a String
angle - read only - the angle provided to the VR parameters
currentAngle - read only - the angle at which the device has been rotated since the start of the app
content - the original content provided to the VR parameters
contentLeft - the content - this shows in the left VR Channel
	The following read-only property is provided on all children of the content:
		vrMatch - a reference to the matching child in the other VR Channel
	The following read-only properties are provided to children of the content that have a depth property
	These are given for content passed in to the content parameter and also to items passed in to the register() mehtod:
		vrChannel - "left" or "right"
		vrStartX - the starting x position before depth effect or parallax is added (could be changed if manually setting x position)
		vrParallaxDistance - the shift in x due to parallax
		vrParallaxDamp - the ZIM Damp object to handle parallax if VR angle is != 0
contentRight - the cloned content - this shows in the right VR Channel (also see contentLeft info)
left - the container that holds the left VR Channel
right - the container that holds the right VR Channel
adjuster - the adjuster pannel - see showAdjuster() method for details
swiper - the ZIM Swiper if swiper parameter is set to true to test the motion if not on device
boundaryLeft - the left boundary container if boundary parameter is true
boundaryRight - the right boundary container if boundary parameter is true

EVENTS
boundaryout - dispatched when rotated past a boundary region - the event object provides a boundary property with "left" or "right" values
boundaryin - dispatched when rotated into the boundary region - the event object provides a boundary property with "left" or "right" values
saved - dispatch when OK button on adjuster pannel is pressed to set the interocular distance
closed - dispatches when X and OK button is pressed to close the adjuster pannel
--*///+69.98
    zim.VR = function (content, angle, distance, parallax, parallaxAngle, damp, parallaxDamp, startAngle, negativeParallax, boundaryMarkers, swiper, holder) {
        var sig = "content, angle, distance, parallax, parallaxAngle, damp, parallaxDamp, startAngle, negativeParallax, boundaryMarkers, swiper, holder";
        var duo; if (duo = zob(zim.VR, arguments, sig, this)) return duo;
		z_d("69.98");
        this.type = "VR";
        if (!zimDefaultFrame) {if (zon) zog("zim.VR() - please use ZIM Frame"); return;}
        var frame = zimDefaultFrame;
        if (zot(holder) || !holder.getBounds || !holder.getBounds().width) var holder = frame.stage;
        if (zot(angle)) angle = 0;
        if (zot(distance)) distance = 100;
        if (zot(damp)) damp = .5;
        if (zot(parallax)) parallax = 0;
        if (zot(parallaxAngle)) parallaxAngle = 60; // parallax operates to 30 degrees on each side of object
        if (zot(parallaxDamp)) parallaxDamp = .1;
        if (zot(startAngle)) startAngle = 0;
        if (zot(negativeParallax)) negativeParallax = false;
        if (zot(boundaryMarkers)) boundaryMarkers = true;
		if (zot(swiper)) swiper = true;
        this.angle = angle;
        this.currentAngle = startAngle;
		if (zim.mobile()) swiper = false;

        var that = this;

        var holderW = holder.width;
        var holderH = holder.height;
        that.zimContainer_constructor();
        var left = that.left = new zim.Container(holderW/2, holderH, null, null, false);
        var backing = new zim.Rectangle(holderW/2, holderH, "rgba(0,0,0,.01)", null, null, null, null, null, false);
        left.addChild(backing);
        var right = that.right = left.clone();
        that.addChild(left, right);
        right.x = holderW/2;

        var contentLeft = that.contentLeft = that.content = content;
        var contentRight = that.contentRight = content.clone();
        if (startAngle != 0) var startAngleTimeout = setTimeout(addContent, 100);
        else addContent();
        function addContent() {
            contentLeft.addTo(left, null, false).setMask(left.getChildAt(0)).pos({y:(holder.height-contentLeft.height)/2, reg:true});
            contentRight.addTo(right, null, false).setMask(right.getChildAt(0)).pos({y:(holder.height-contentLeft.height)/2, reg:true});
        };

        var leftVR = []; // lists of eligable containers
        var rightVR = [];
        function copyDepth(left, right, registerLeft, registerRight) {
            left.vrMatch = right;
            right.vrMatch = left;
            if (left.cacheCanvas) right.cache();
            if (left.dep && !zot(left.depth)) {
                leftVR.push(left);
                rightVR.push(right);
                if (!zot(registerLeft)) registerLeft.push(left);
                if (!zot(registerRight)) registerRight.push(right);
                right.dep(left.depth);
            }
            // recursive
            zim.loop(left, function(left, i){
                copyDepth(left, right.getChildAt(i), registerLeft, registerRight);
            });
        }
        copyDepth(contentLeft, contentRight);

        function setDepth(list, channel) {
            zim.loop(list, function(container) {
                container.vrChannel = channel;
                container.vrStartX = container.x;
                if (angle != 0 && distance != 0) {
                    container.vrAngle = angle;
                    container.vrDistance = distance;
                }
                if (parallax != 0 && distance != 0) {
                    container.vrParallax = parallax/100;
                    container.vrParallaxDistance = 0;
                    if (angle != 0) container.vrParallaxDamp = new zim.Damp(0, parallaxDamp);
                }
                container.dep(container.depth);
            });
        }
        setDepth(leftVR, "left");
        setDepth(rightVR, "right");

        if (angle != 0 && distance != 0) {
            // overall motion of content due to head turn
            var pd = new zim.Damp(180+startAngle, damp);
            pd.immediate(180+startAngle);
			var minCheck = false;
			var maxCheck = false;
            frame.on("deviceorientation", function(e) {
                moveVR(pd.convert((e.rotation.z > 180 ? -180 : 180) + e.rotation.z + startAngle)-180);
			});
			function moveVR(a) {
				that.currentAngle = a;
                var d = -that.currentAngle * distance / angle;
                that.contentLeft.x = d;
                that.contentRight.x = d;
				if (Math.round(that.currentAngle) <= -angle/2 && !minCheck) {
					var ev = new createjs.Event("boundaryout");
					ev.boundary = "left";
					minCheck = true;
					that.dispatchEvent(ev);
				} else if (Math.round(that.currentAngle) >= angle/2 && !maxCheck) {
					var ev = new createjs.Event("boundaryout");
					ev.boundary = "right";
					maxCheck = true;
					that.dispatchEvent(ev);
				} else if (Math.round(that.currentAngle) > -angle/2 && Math.round(that.currentAngle) < angle/2) {
					var ev = new createjs.Event("boundaryin");
					if (minCheck) {
						ev.boundary = "left";
						that.dispatchEvent(ev);
						minCheck = false;
					} else if (maxCheck) {
						ev.boundary = "right";
						that.dispatchEvent(ev);
						maxCheck = false;
					}
				}
            }
            zim.Ticker.always();

            function calculateDistance(container) {
                var point = container.parent.localToLocal(container.vrStartX, 0, holder);
                var d = container.vrParallaxDamp.convert(container.vrChannel == "left" ? that.contentLeft.x + point.x - holderW / 4 : that.contentRight.x + point.x - holderW / 2 - holderW / 4);
                if (container.depth <= 0 && !negativeParallax) d = 0;
                container.vrParallaxDistance = Math.max(-distance/angle*parallaxAngle/2, Math.min(distance/angle*parallaxAngle/2, d));
                container.dep(container.depth);
            }
        }
        if (parallax != 0 && distance != 0) {
            var parallaxPD = new zim.ProportionDamp(180-parallaxAngle/2, 180+parallaxAngle/2, -parallaxAngle/2, parallaxAngle/2, parallaxDamp);
			parallaxPD.immediate(180);
            if (angle == 0) {
                function calculateDistance2(container, a) {
                    container.vrParallaxDistance = (container.depth <= 0 && !negativeParallax) ? 0 : a;
                    container.dep(container.depth); // set the depth again now that the parallax Distance has changed
                }
            }
            frame.on("deviceorientation", function(e) {
				// only parallax based on distance, parallax factor and parallax angle - no content movement
				var a = 0;
				if (angle == 0 && parallax !=0 && distance !=0) a = parallaxPD.convert((e.rotation.z > 180 ? -180 : 180) + e.rotation.z);
				doParallax(a);
			});

			function doParallax(a) {
                if (angle != 0 && distance != 0) { // movement and parallax
                    // content movement so base parallax on distance of content from center of view
                    zim.loop(leftVR, calculateDistance);
                    zim.loop(rightVR, calculateDistance);
                } else { // only parallax
                    zim.loop(leftVR, function(container) {calculateDistance2(container, a)})
                    zim.loop(rightVR, function(container) {calculateDistance2(container, a)});
                }
            }
            zim.Ticker.always();
        }

		if (swiper && distance !=0 && (angle !=0 || parallax != 0)) {
			that.added(function() {
				var swipeObject = {swipeAngle:0};
				var swiper = that.swiper = new zim.Swiper({
					swipeOn:that,
					target:swipeObject,
					property:"swipeAngle",
					sensitivity:.2,
					damp:.05,
					factor:-1,
					min:-330,
					max:330
				});
				Ticker.add(function() {
					// if (swipeObject.swipeAngle > 180) {
					// 	swipeObject.swipeAngle -= 360;
					// 	swiper.immediate(swipeObject.swipeAngle);
					// 	parallaxD.immediate(swipeObject.swipeAngle);
					// 	pd.immediate(swipeObject.swipeAngle);
					// }
					// if (swipeObject.swipeAngle < -180) {
					// 	swipeObject.swipeAngle += 360;
					// 	swiper.immediate(swipeObject.swipeAngle);
					// 	parallaxD.immediate(swipeObject.swipeAngle);
					// 	pd.immediate(swipeObject.swipeAngle);
					// }
					if (angle != 0 && distance != 0) moveVR(swipeObject.swipeAngle);
					if (parallax != 0 && distance != 0) doParallax(parallaxPD.convert(swipeObject.swipeAngle+180));
				});
			});
		}


		// ADJUSTER INTERFACE
		// localStorage.clear();
		var eyeAdjust = localStorage && localStorage.zimEyeAdjust ? Number(localStorage.zimEyeAdjust) : 0;
		var lastEyeAdjust = eyeAdjust;
		var adjusterCheck = false;
		contentLeft.startRegX = contentLeft.regX;
		contentRight.startRegX = contentRight.regX;
		var adjuster = this.adjuster = new zim.Container(800, 300, null, null, false);
		var backing = adjuster.backing = new zim.Rectangle(adjuster.width, adjuster.height, zim.lighter, null, null, null, null, null, false).center(adjuster).alp(1).sha("rgba(0,0,0,.2)", 0, 0, 30);
		var backing2 = adjuster.strip = new zim.Rectangle(adjuster.width, adjuster.height/3, zim.white, null, null, null, null, null, false).center(adjuster).alp(1);
		var label = adjuster.label = new zim.Label({
			text:"slide to adjust center of left and right",
			size:28,
			color:zim.dark,
			align:"center",
			valign:"center"
		}).center(adjuster).pos({y:50, reg:true});

		var close = adjuster.close = new zim.Rectangle(50,50,zim.light, null, null, null, null, null, false).addTo(adjuster).mov(adjuster.width-70, 26)
		var ex = new zim.Shape(-40,-40,80,80,null,false);
		ex.graphics.f(zim.dark).p("AmJEVIEUkTIkXkWIB4h5IEWEYIETkTIB4B3IkTESIEQERIh4B4IkRkRIkSEVg"); // width about 90 reg in middle
		ex.center(close).sca(.3);
		close.cursor = "pointer";
		close.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", function() {
			that.hideAdjuster();
			slider.currentValue = (slider.max - slider.min) / 2 + lastEyeAdjust;
			that.dispatchEvent("closed");
		});

		var dotLeft = new zim.Circle(30, zim.dark, null, null, null, null, null, false).center(adjuster).pos({x:adjuster.width/4, reg:true})
		dotLeft.shape.alpha = .2
		new zim.Circle(12, zim.dark, null, null, null, null, false).center(dotLeft)
		var dotRight = new zim.Circle(30, zim.dark, null, null, null, null, false).center(adjuster).pos({x:adjuster.width/4*3, reg:true})
		dotRight.shape.alpha = .2
		new zim.Circle(12, zim.dark, null, null, null, null, null, false).center(dotRight)
		var slider = adjuster.slider = new zim.Slider({
			min:0, max:30, step:1, useTicks:true, style:false
		}).centerReg(adjuster).pos({y:adjuster.height-40, reg:true});
		slider.currentValue = (slider.max - slider.min) / 2 + eyeAdjust;
		var leftP = new zim.Proportion(slider.min, slider.max, dotLeft.x-adjuster.width/4, dotLeft.x+adjuster.width/4, -1);
		var rightP = new zim.Proportion(slider.min, slider.max, dotRight.x-adjuster.width/4, dotRight.x+adjuster.width/4);
		slider.on("change", function() {
			dotLeft.x = leftP.convert(slider.currentValue);
			dotRight.x = rightP.convert(slider.currentValue);
		})
		new zim.Label("closer", 24, null, zim.silver).centerReg(adjuster).pos({x:slider.x - 220, y:slider.y, reg:true});
		new zim.Label("farther", 24, null, zim.silver).centerReg(adjuster).pos({x:slider.x + 220, y:slider.y, reg:true});
		var ok = that.ok = new zim.Button({
			label:"OK",
			width:90,
			height:60,
			corner:0,
			backgroundColor:zim.blue,
			rollBackgroundColor:zim.green,
			shadowColor:-1,
			style:false
		}).centerReg(adjuster).sca(.8).pos({x:adjuster.width-58, y:slider.y, reg:true});

		ok.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", function() {
			if (localStorage) localStorage.zimEyeAdjust = slider.currentValue - (slider.max - slider.min) / 2;
			setReg();
			that.hideAdjuster();
			that.dispatchEvent("saved");
			that.dispatchEvent("closed");
		});

		var zero = that.zero = new zim.Button({
			label:">|<",
			width:90,
			height:60,
			corner:0,
			backgroundColor:zim.yellow,
			rollBackgroundColor:zim.green,
			shadowColor:-1,
			style:false
		}).centerReg(adjuster).sca(.8).pos({x:58, y:slider.y, reg:true});
		zero.on((!zns?ACTIONEVENT=="mousedown":zim.ACTIONEVENT=="mousedown")?"mousedown":"click", function() {
			slider.currentValue = (slider.max - slider.min) / 2;
			dotLeft.x = leftP.convert(slider.currentValue);
			dotRight.x = rightP.convert(slider.currentValue);
		});

		var adjusterP = new zim.Proportion(0, 30, -holder.width/4, holder.width/4);
		function setReg() {
			// shift the registration point of the content to accommodate eye distance adjust
			var change = adjusterP.convert(slider.currentValue);
			contentLeft.regX = contentLeft.startRegX + change;
			contentRight.regX = contentRight.startRegX - change;
		}
		setReg();

		this.showAdjuster = function() {
			if (adjusterCheck) return that;
			adjusterCheck = true;
			dotLeft.x = leftP.convert(slider.currentValue);
			dotRight.x = rightP.convert(slider.currentValue);
			lastEyeAdjust = slider.currentValue - (slider.max - slider.min) / 2;
			adjuster.scaleTo(holder).center(this);
			holder.stage.update();
		}

		this.hideAdjuster = function() {
			if (!adjusterCheck) return that;
			adjusterCheck = false;
			adjuster.removeFrom(this);
			holder.stage.update();
		}

        this.position = function(item, x, y) {
			posit(item);
			posit(item.vrMatch);
			function posit(it) {
				it.vrStartX = x;
	            if (it.vrParallaxDamp) it.vrParallaxDamp.immediate(x);
	            it.mov(x, y);
			}
            return item;
        }

        this.register = function(item) {
            // object must be added to content container
            if (!item.parent || !item.parent.vrMatch) {
                if (zon) {zog("ZIM VR() - please only register objects already inside content container")}
                return item;
            }
			var itemRight = item.clone().addTo(item.parent.vrMatch);

            var registerLeft = [];
            var registerRight = [];
            copyDepth(item, itemRight, registerLeft, registerRight);
            setDepth(registerLeft, "left");
            setDepth(registerRight, "right");
            return item;
        }

        if (angle != 0 && distance != 0 && boundaryMarkers) {
            that.boundaryRight = new zim.Container({style:false});
            new zim.Circle(24, zim.light, null, null, null, null, null, false).addTo(that.boundaryRight).dep(-2);
            new zim.Triangle(16,16,16,zim.yellow, zim.grey,null,null,-3,null,null,false).rot(-90).center(that.boundaryRight).dep(-6);
            that.boundaryRight.center(content).mov(distance/2+24);
            that.boundaryLeft = new zim.Container({style:false});
            new zim.Circle(24, zim.light, null, null, null, null, null, false).addTo(that.boundaryLeft).dep(-2);
            new zim.Triangle(16,16,16,zim.yellow, zim.grey,null,null,-3,null,null,false).rot(90).center(that.boundaryLeft).dep(-6);
            that.boundaryLeft.center(content).mov(-distance/2-24);
            that.register(that.boundaryRight);
            that.register(that.boundaryLeft);
        }

        function removeItem(item) {
            var index = leftVR.indexOf(item);
            if (leftVR.indexOf(item) >= 0) {
                leftVR.splice(index, 1);
                rightVR.splice(index, 1);
            }
            zim.loop(item, function(inside) {
                removeItem(inside);
            });
        }

        this.remove = function(items) {
            // call to remove an item from VR
            // Note: arguments is an object with properties matching the index of the argument
            zim.loop(arguments, function(index, item) {
                if (!content.contains(item)) return;
                removeItem(item);
                item.parent.removeChild(item);
                item.vrMatch.parent.removeChild(item.vrMatch);
            });
            return that;
        }

        this.resize = function() {
            holderW = holder.width;
            holderH = holder.height;
            left.getChildAt(0).widthOnly = right.getChildAt(0).widthOnly = holderW/2;
            left.getChildAt(0).heightOnly = right.getChildAt(0).heightOnly = holderH;
            right.x = holderW/2;
            left.setBounds(0,0,holderW/2,holderH);
            right.setBounds(0,0,holderW/2,holderH);
            if (startAngle != 0) {
                clearTimeout(startAngleTimeout);
                startAngleTimeout = setTimeout(addContent, 100);
            }
            else addContent();
			adjuster.scaleTo(holder).center(this, null, false);
        }
    }
    zim.extend(zim.VR, zim.Container, "clone", "zimContainer", false);
	//-69.98


////////////////  ZIM FRAME  //////////////

// Zim Frame provides code to help you set up your coding environment

/*--
zim.Frame = function(scaling, width, height, color, outerColor, assets, path, progress, rollover, touch, scrollTop, align, valign, canvasID, rollPerSecond, delay, canvasCheck, gpu, gpuObj, nextFrame, nextStage, allowDefault, loadFailObj, sensors, retina)

Frame
zim class - extends a createjs EventDispatcher

DESCRIPTION
Frame creates a canvas and stage.
Frame lets you decide how you want your stage to scale.
It also provides events for ready, resizing and orientation change
as well as a way to remake the canvas if necessary.
Frame handles loading Bitmap and Sound assets by wrapping PreloadJS
see https://zimjs.com/frame.html for sample templates using Frame.

CANVAS ALTERNATIVE CONTENT
Frame will move any tag with an id of canvasIDAlternative into the canvas tag
By default, the canvasID is "myCanvas" so use an id of "myCanvasAlternative"
This allows you to show content for non-canvas browsers
The content may also be indexed by search engines - one would hope
and is read by screen readers (see also ZIM Accessibility)

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var frame = new Frame("fit", 1024, 768);
frame.on("ready", function() {
	var stage = frame.stage;
	var stageW = frame.width;
	var stageH = frame.height;
	frame.outerColor = dark; // setting colors here lets you use frame colors
	frame.color = light;

	// code here - or optionally load assets

	frame.loadAssets("image.png");
	frame.on("complete", function() {

		// app code goes here if waiting for assets
		var image = frame.asset("image.png");
		image.center(stage);
		stage.update();

	}); // end asset complete

	// OR for multiple assets in an assets folder:

	frame.loadAssets(["sound.mp3", "spriteData.json", "spriteImage.png"], "assets/");
	frame.on("complete", function() {

		// app code goes here if waiting for assets
		var soundInstance = frame.asset("sound.mp3").play();
		// later soundInstance.paused = true; // etc.

		var sprite = new Sprite({json:frame.asset("spriteData.json")});
		sprite.center(stage).run(2000);
		// the image for the sprite is specified in the JSON
		// but we still want to load it so it is in the loadAssets()
		// and the JSON data will take care of adding it to the sprite

		stage.update();

	}); // end asset complete

}); // end of ready

END EXAMPLE

EXAMPLE
	// With multiple loadAsset() calls you can assign the results to a variable
	// and use that variable for the events independently
	// Warning, each of these will still call a frame complete event
	// so usually you would use one or the other but not both

	var first = frame.loadAssets("image.png");
	first.on("complete", function() {
		var image = frame.asset("image.png").center(stage);
	});

	var second = frame.loadAssets("sound.mp3");
	second.on("complete", function() {
		var sound = frame.asset("sound.mp3").play();
	});

END EXAMPLE

EXAMPLE
// load assets right in Frame call
// see the assets and path and progress are optional

var frame = new Frame("fit", 1024, 768, "#ddd", "#333", "image.png", "assets/", new Waiter());
frame.on("ready", function() {
	var stage = frame.stage;
	var stageW = frame.width;
	var stageH = frame.height;

	frame.asset("image.png").center();

	stage.update();
}); // end ready
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
scaling - (default "full") can have values as follows
	"fit"      sets canvas and stage to dimensions and scales to fit inside window size
	"outside"  sets canvas and stage to dimensions and scales to fit outside window size
	"full"     sets stage to window size with no scaling
	"tagID"    add canvas to HTML tag of ID - set to dimensions if provided - no scaling

FIT and OUTSIDE: width and height will set the stage width and height and the canvas is then scaled
this is handy because all your dimensions are set to start
FULL: width and height are ignored when scaling as these are set to the window width and height
TAG: if width and height are provided then the canvas and stage will be these dimensions
if width and height are not provided in tag mode, the canvas and stage will take the dimensions of the tag
this means, the tag must have some sort of width and height dimensions set or it will be really big!

width, height (default null for fit and tagID or 500, 500 for fit and outside) the width and height for the stage
color - (default null) the background color of the frame (any CSS value) - or just set in styles
	will be see-through if not specified
outerColor - (default null) the body color of the HTML page - null will not adjust the background color
assets - (default null) - 1. a string asset or 2. an array of assets or 3. an assets config object
	1. "logo.png"
	2. ["logo.png", "bounce.mp3"]
	3. {asset:"logo.png", path:"assets/", loadTimeout:2000}
	see the loadAssets() method for details
	NOTE: "complete", "progress" and "fileLoaded" events are not dispatched
		use loadAssets() for these if desired
		the "ready" event will be dispatched when the canvas is ready and initial assets are loaded
	NOTE: the loadAssets() method can still be used as desired
path - (default null) - a string path for the assets
	if an assets object is provide with a path then this parameter is ignored
progress - (default null) - set to a Waiter() or ProgressBar() object to show while loading
rollover - (default true) activates rollovers
touch - (default true) activates touch on mobile
scrollTop - (default false) activates scrolling on older apple devices to hide the url bar
align - (default "center") for fit and outside, the horizontal alignment "left", "center/middle", "right"
valign - (default "center") for fit and outside, the vertical alignment "top", "center/middle", "bottom"
canvasID - (default "myCanvas" or if subsequent frame, myCanvas+randomID) will be set to tagIDCanvas if a tagID is provided - eg. scaling="test", canvasID="testCanvas"
rollPerSecond - (default 20) times per second rollover is activated (if rollover parameter is true)
delay - (default 500) time in milliseconds to resize ONCE MORE after a orientation change
	unfortunately, some older devices may have a delay (after a window resize event) in reporting screen sizes
	so a time of 500 or so might catch the dimension change then call the frame resize event with the proper dimensions
	setting this may cause a flash on faster devices that do not need it - so it is a no win situation
	this effects only full mode with the Layout class and they can always refresh a screen if it is not quite right in the changed orientation
canvasCheck - (default true) check to see if there is canvas support - uses !!window.HTMLCanvasElement
gpu - (default false) set to true to use a CreateJS StageGL stage for GPU renderer
 	See: http://blog.createjs.com/stagegl-faster-better-stronger-webgl-update-easeljs/ (written before version 1 release)
	Use CreateJS 1.0.0 or later to get StageGL.
	https://github.com/CreateJS/Combined/tree/master/builds/1.0.0
	Can use http://d309knd7es5f10.cloudfront.net/createjs.min.js (CreateJS 1.0.0 until they host it on their CDN)
gpuObj - (default null) object with following properties (with defaults) See CreateJS docs on GITHUB:
	preserveBuffer (false), antialias (false), transparent (false), premultiply (false), autoPurge (1200)
nextFrame - (default null) set to zim Frame object of Frame underneath current Frame to pass events to nextFrame
nextStage - (default null) alternative to nextFrame if the stage beneath current Frame is not a ZIM Frame but just a CreateJS Stage
allowDefault - (default false - true for tag mode) set to true to allow default mouse, key and scrollwheel events on canvas
	See also the zil property of frame that allows you to add and remove these events dynamically (except for mouse swipe scroll and zoom on mobile)
	allowDefault of false also sets body overflow to hidden - which is good for full, fit and outside modes
	also see allowDefault property
loadFailObj - (default result of frame.makeCircles) object that shows if asset() does not exist or did not load withing loadTimeout
	This will be given a type property of "EmptyAsset"
	Set the loadFailObj property below to null to set no object - but this will yield errors unless each resulting asset() is tested
	Set to new Container() to show nothing (but avoid errors) - or new Rectangle(10, 10) to show little black square, etc.
sensors - (default false) set to true to capture Frame devicemotion and deviceorientation events - see Events
retina - (default true) scales stage to use pixelDensity (sharper when scaling up) and supports Adobe Animate export
	may need to set mouse event targets to e.stageX/stage.scaleX and e.stageY.stage.scaleY
	except for using stage.getObjectUnderPoint()
	ZIM overrides CreateJS localToGlobal, globalToLocal and localToLocal to accomodate stage scaling
	This was a major adjustment to transform(), bezier controls, outline, physics, etc.
	set to false to return to traditional PRE ZIM 10.3.0 unscaled stage

METHODS
loadAssets(assets, path, xhr, time, loadTimeout, outputAudioSprite, crossOrigin, fileType, queueOnly) // also accepts ZIM DUO configuration object as single parameter
	// see also assets and path parameters of Frame - which share the info below
	assets - a file (url String or asset object) or files in an Array
		each asset String is how you then access the asset with the asset() method of Frame
		asset types (from CreateJS PreloadJS): Image, JSON, Sound, SVG, Text, XML
		asset can also be an asset object:
			{id:string, src:url or filename (if specify path with path parameter)}
			then can use the id to access the asset in the asset() method of Frame
		asset can also be a font object:
			{font:name, src:url, type:string, weight:string, style:string} // with last three properties being optional
			eg.
			{font: "wildwood", src:"ChurchintheWildwood-Regular.ttf", type:"OpenType"} // type is not needed
			{font: "regu", src:"regul-bold.woff", weight:"bold"}
			{src:"https://fonts.googleapis.com/css?family=Roboto"}
			For google fonts you add extra information to the url so the font (family), type, weight and style are ignored
			If absolute src is used, path parameter is ignored - otherwise path is added to start of src
			After loading, can just use:
				var label = new Label("hello", 30, "wildwood") // or whatever the font property is
		asset can be an AudioSprite - which is a single sound file and data for sounds within the sound file:
			ZIM has a format for the data and so does CreateJS - there is also the parseAudioSprite() method for importing formats
			See the parseAudioSound parameter to pre-parse the ZIM format then use the resulting CreateJS format to avoid live parsing (maybe save a millisecond)
			ZIM FORMAT:
				{src:"audiosprite.mp3", audioSprite:[
	                // [id, startime(s), endtime(s)]
	                // prefer this when making audioSprites by hand in Premiere or Audition
	                ['blackball', 1.041, 2.475],
	                ['bounce', 3.567, 4.232]
				]}
			CREATEJS FORMAT:
				{src: "audiosprite.mp3", data:{ // extra data property
				    audioSprite: [
				        {id:"sound1", startTime:0, duration:500}, // time in ms
				        {id:"sound2", startTime:1000, duration:400},
				        {id:"sound3", startTime:1700, duration: 1000}
				    ]
				}}
	path - pass in an optional path String that gets prepended to the asset
		when accessing the asset with the asset() method you do NOT include the path
	progress - (default null) - set to a Waiter() or ProgressBar() object to show while loading
	xhr (default false or true if Progress is a ProgressBar) set to true to load text and WebAdio (not needed for normal sound mp3, wav, etc.)
	time (default 0) is the minimum number of milliseconds for the complete event to trigger
		use this for testing or to always have time to show a loading message
	loadTimeout (default 8000) is how many ms to wait for asset before error and a complete fires even though asset not loaded
	outputAudioSprite (default false) set to true when passing in a ZIM AudioSprite format to output to the console a CreateJS AudioSprite JSON object
	 	JSON.parse() this object before passing in to loadAssets() - and add single quotes around console output as those are stripped by console
	crossOrigin (default "anonymous") - leave at default to load from Amazon S3, etc.
		had to add <AllowedMethod>HEAD</AllowedMethod> in CORSRule of CORS configuration on Amazon S3 for fonts
		https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html#how-do-i-enable-cors
		https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-cors
		and had to edit distribution behaviours for cache header > whitelist and move over Origin
	fileType (default null) in cases where the file type cannot be parsed or is parsed incorrectly, this overrides
		might have to split up loading as this fileType gets applied to all files loaded
		Types are for CreateJS PreloadJS: https://www.createjs.com/docs/preloadjs/classes/LoadQueue.html
		    createjs.Types.BINARY: Raw binary data via XHR
		    createjs.Types.CSS: CSS files
		    createjs.Types.IMAGE: Common image formats
		    createjs.Types.JAVASCRIPT: JavaScript files
		    createjs.Types.JSON: JSON data
		    createjs.Types.JSONP: JSON files cross-domain
		    createjs.Types.MANIFEST: A list of files to load in JSON format, see AbstractLoader/loadManifest
		    createjs.Types.SOUND: Audio file formats
		    createjs.Types.SPRITESHEET: JSON SpriteSheet definitions. This will also load sub-images, and provide a SpriteSheet instance.
		    createjs.Types.SVG: SVG files
		    createjs.Types.TEXT: Text files - XHR only
		    createjs.Types.VIDEO: Video objects
		    createjs.Types.XML: XML data
	queueOnly (default false) - set to true to send events only to the Queue object (see below) rather than the Frame
		when using queues to accept events, remember that the frame also receives events
		if you also have some general frame events for loading, set this parameter to true to avoid accident
	RETURNS: a Queue object that can be used for control with multiple loadAssets calls
	Each Queue will trigger progress, assetload and complete events
	Each Queue will have a preload property to the CreateJS LoadQueue and an isLoading property
	The frame also has these events and properties but acts for all loading - so be careful - see the queueOnly parameter
	It is recommended to use the Queue any time you use multiple LoadAssets() calls at the same time
	You still access assets with frame.asset() as outlined below whether you use the Queue or not
asset(file) - access a loaded asset based on file string (not including path)
	if the asset uses an asset object with an id then you can use the id
	if the asset is an image then this is a Bitmap and you add it to the stage
	if the asset is a sound then use asset(file).play();
		or can pass in a configuration object in play
		with the following properties (see CreateJS SoundJS docs)
		delay, offset, loop, loopCount, volume, pan, startTime, interrupt and duration
		// note: ZIM has adjusted loop (Boolean) and added loopCount (number) to be consistent with animate() parameters
		asset(file).play({volume:.5, pan:-1, loop:true}); // or loopCount:10 for a specific number of loops or loop:10 also works
		this returns createjs sound instance which can also be manipulated
		to stop the sound or set its volume dynamically, etc.
	if the asset is anything else, then it is what it is!
makeCircles(radius, multiple) - returns ZIM Circles (centered reg)
	radius defaults to 100
	multiple defaults to false which will return a ZIM Shape - set to true to return a ZIM Container of ZIM Circles
remakeCanvas(width, height) - removes old canvas and makes a new one and a new stage
	will have to set your local stage, stageW and stageH variables again
dispose() - removes canvas, resize listener and stage

PROPERTIES
type - holds the class name as a String
stage - read only reference to the zim Stage - to change run remakeCanvas()
	frame gives the stage read only stage.width and stage.height properties
canvas - a reference to the frame's canvas tag
canvasID - a reference to the frame's canvas ID
color - the color of the frame background - any css color
outerColor - the color of the body of the HTML page - set with styles
tag - the containing tag if scaling is set to an HTML tag id (else null)
isLoading - a Boolean to indicate whether loadAssets() is currently loading assets (also, each queue has an isLoading property)
width - read only reference to the stage width - to change run remakeCanvas()
height - read only reference to the stage height - to change run remakeCanvas()
scale - read only returns the scale of the canvas - will return 1 for full and tag scale modes
orientation - "vertical" or "horizontal" (updated live with orientation change)
visibleLeft, visibleTop, visibleRight, visibleBottom - in "outside" scale mode these give window edge locations relative to the stage
	can be used to position items like navigation relative to window as the frame resize event is fired
	in all scale modes other than "outside", the values are 0, stageW, 0, stageH
zil - reference to zil events that stop canvas from shifting or scrolling - also see allowDefaults parameter
	can set allowDefault property to false then allow specific defaults by removing zil events - see zil global function
	example: window.removeEventListener("keydown", frame.zil[0]); removes keydown preventions (for page up, page down, home, end, etc)
allowDefault - set to true to remove zil or false to set zil (see above) also affects body overflow
colors: orange, green, pink, blue, brown, yellow, red, purple, silver, tin, grey, lighter, moon, light, dark, darker, white, black, clear (0 alpha), faint (.01 alpha)
altKey - true if the alt key is being pressed otherwise false
ctrlKey - true if the ctrl key is being pressed otherwise false
metaKey - true if the meta key (âŒ˜ command on Mac or âŠž windows key) is being pressed otherwise false
shiftKey - true if the shift key is being pressed otherwise false
loadFailObj - the object that shows if images are broken - will be given a type property of "EmptyAsset"
retina - read-only Boolean as to whether stage (as opposed to the canvas) was scaled for pixelDensity during Frame creation

EVENTS
"ready" - fired when the stage is made
"failed" - fired if no canvas support (and canvasCheck parameter is set to true - which is the default)
"resize" - fired on resize of screen
"orientation" - fired on orientation change
"keydown" - fired on keydown - just like the window keydown event with eventObject.keyCode, etc.
	also stores frame.altKey, frame.ctrlKey, frame.metaKey, frame.shiftKey
"keyup" - fired on keyup - just like the window keyup event with eventObject.keyCode, etc.
"wheel" - fired on mousewheel (Window wheel event)
	can get eventObject.deltaX and eventObject.deltaY
	these vary greatly in value based on Browser
	may want to just ask for sign(eventObject.deltaY) and multiply it by a factor
	and then perhaps constrain the value - here the scale is constrained between .5 and 5
	note - when changing scale, it is better to multiply by a factor rather than add to the scale
	eg. circle.scale = constrain(circle.scale*(sign(e.deltaY)>0?.75:1.25), .5, 5);
"deviceorientation" - MUST SET Frame sensors parameter to true
	fired as device orientation changes:
	eventObject.rotation.x (beta in HTML specs) holds rotation about the x axis between -180 and 180 (tipped forward or backward)
	eventObject.rotation.y (gamma in HTML specs) holds rotation about the y axis between -90 and 90 (tipped left or right)
	eventObject.rotation.z (alpha in HTML specs) holds rotation about the z axis 0-360 clockwise (relative to orientation when app loads)
		note rotation.z is 360-alpha compared to the HTML 5 specs
		note also that beta, gamma and alpha from the HTML 5 specs are also provided
	eg. frame.on("deviceorientation", function(e) {zog(e.rotation.x, e.rotation.y, e.rotation.z)});
"devicemotion" - MUST SET Frame sensors parameter to true
	fired on moving mobile device - like a tilt or shake - eventObject.acceleration holds x, y and z properties of motion
	eg. frame.on("devicemotion", function(e) {zog(e.acceleration.x, e.acceleration.y, e.acceleration.z)});

ASSET EVENTS
loadAssets() will trigger these events on the Frame object and on the specific queue (eg. var queue = frame.loadAssets();)
NOTE: if loadAssets() queueOnly parameter is true, then only the queue receives the events - see queueOnly parameter
"progress" - fires constantly as assets are loaded with loadAssets() to represent overall load progress (fonts not included)
	the event object has a progress property between 0 and 1
"assetload" - fired when an asset loaded with loadAssets() has loaded (use asset property of event object - with type and id properties) (fonts not included)
"complete" - fired when all assets loaded with loadAssets() are loaded (then use frame.asset())
"error" - fired when there is a problem loading an asset with loadAssets()

--*///+83
	zim.Frame = function(scaling, width, height, color, outerColor, assets, path, progress, rollover, touch, scrollTop, align, valign, canvasID, rollPerSecond, delay, canvasCheck, gpu, gpuObj, nextFrame, nextStage, allowDefault, loadFailObj, sensors, retina) {
		var sig = "scaling, width, height, color, outerColor, assets, path, progress, rollover, touch, scrollTop, align, valign, canvasID, rollPerSecond, delay, canvasCheck, gpu, gpuObj, nextFrame, nextStage, allowDefault, loadFailObj, sensors, retina";
		var duo; if (duo = zob(zim.Frame, arguments, sig, this)) return duo;
		z_d("83");
		if (zon) zog("ZIM FRAME");
		this.cjsEventDispatcher_constructor();
		this.type = "Frame";
		var that = this;

		// global variables to help reference the frame in some cases
		// not currently used for stage critical functions
		// just used to get key events or colors internally
		// also, as of zim 5.1.0 used for Ticker.add(function(){});
		// will use the zimDefaultFrame.stage if stage is not supplied as second parameter
		// this may lead to problems if a second frame is used
		// and the second frame stage is not used for the Ticker.add()
		// but two frames are rarely used and this simplifies 99% of the tickers
		if (typeof zimDefaultFrame == 'undefined') zimDefaultFrame = this;

		if (zot(canvasCheck)) canvasCheck = true;
		var canvasSupported = !!window.HTMLCanvasElement;
		if (!canvasSupported && canvasCheck) { // no canvas support
			setTimeout(function() {
				that.dispatchEvent("failed");
			}, 100);
			return;
		}
		var mobile = zim.mobile();
		if (zot(scaling)) scaling = "full";
		if (zot(rollover)) rollover = !mobile;
		if (zot(touch)) touch = true;
		if (zot(scrollTop)) scrollTop = false;
		if (zot(align)) align = "center";
		if (zot(valign)) valign = "center";
		var randomCanvas = "";
		if (zot(canvasID)) {
			if (zimDefaultFrame != this) { // more than one canvas with generic name
				randomCanvas = zim.makeID(5);
				canvasID = "myCanvas" + randomCanvas;
			} else {
				canvasID = "myCanvas";
			}
		}
		if (zot(rollPerSecond)) rollPerSecond = 20;
		if (zot(delay)) delay = 0;
		if (zot(gpu)) gpu = false;
		if (zot(allowDefault)) allowDefault = false;
		if (zot(loadFailObj)) loadFailObj = "circles";
		this.loadFailObj = loadFailObj;
		if (zot(sensors)) sensors = false;
		if (zot(retina)) retina = true;
		this.retina = retina;

		// with the addition of the assets parameter there are two asynchronous events
		// the canvas making with various timeouts and the asset loading
		// When a process is done, it checks to see if readyToDispatch is true
		// if so it dispatches "ready" otherwise it sets the readyToDispatch to true
		// when other process is done, it does the same
		var readyToDispatch = false;

		// setting a scaling of something other than this list will set the scaling to tag mode
		// where the scaling parameter value is assumed to be the ID of an HTML tag to contain the Frame
		var types = ["fit","outside","full"];

		this.scale = 1;
		this.x = 0;
		this.y = 0;

		var stage;
		var stageW = width;
		var stageH = height;
		var largest; // automatically set
		var appOrientation; // watch out - orientation keyword is used by apple - sigh
		var lastOrientation; // used to detect orientation change
		var appReady = false; // check variable (watch - "ready" is reserved)
		var tagID;
		var tag;

		var initCheck = false;
		if (document.readyState === 'interactive' || document.readyState === 'complete' ) { // DOM has loaded
			setTimeout(function() {init();}, 200); // can't dispatch directly from a constructor
		} else {
			document.addEventListener('DOMContentLoaded', init);
		}

		// Firefox has a glitch when setting the canvas to a new dimension
		// this only happens in full mode if a Ticker is updating the stage
		// so set the Ticker update to false - unfortunately for 500ms
		// which means animations will pause a little during resize
		// the resize event triggers pretty quickly and that will update the stage
		var lastTicker;
		var pauseTicker = false;
		var checkResize = (scaling == "full" && typeof InstallTrigger !== 'undefined'); // firefox check
		window.addEventListener('resize', windowResizeEvent);
		function windowResizeEvent() {
			if (checkResize) {
				if (!pauseTicker) {
					pauseTicker = true;
					lastTicker = zim.Ticker.update;
					zim.Ticker.update = false;
					setTimeout(function() {
						pauseTicker = false;
						// only have one Frame set this...
						if (stage == zimDefaultFrame.stage) zim.Ticker.update = lastTicker;
					}, 40);
					setTimeout(function() {
						sizeCanvas();
						dispatchResize();
					}, 20);
				}
			} else {
				sizeCanvas();
				dispatchResize();
				if (delay > 0) {
					if (mobile) setTimeout(function() {
						sizeCanvas();
						dispatchResize();
					}, delay); // to catch delayed screen sizes
				}
			}
		};

		function init() {
			if (initCheck) return;
			initCheck = true;
			if (types.indexOf(scaling) == -1) {
				tagID = scaling;
				if (zot(zid(tagID))) {zog("zim.Frame - scaling: HTML tag with id="+scaling+" must exist"); return;};
				tag = this.tag = zid(tagID);
				scaling = (zot(width) || zot(height)) ? "tag" : "inline"; // tag with no dimensions or dimensions
				if (canvasID.indexOf("myCanvas") == 0) canvasID = tagID + "Canvas"; // removed in 6.9.0 + randomCanvas;
			}
			that.canvasID = canvasID;

			// now assign default width and height (ignored by full and tag)
			if (zot(width)) width = 500;
			if (zot(height)) height = 500;

			if (!zot(outerColor)) that.outerColor = outerColor;

			makeCanvas();
			makeStage();

			if (mobile) {
				// for older mobile - pan hides the location bar
				if (scrollTop) {setTimeout(function() {window.scrollTo(0, 0);}, 50);}
				setTimeout(function() {
					// on all mobile devices
					// note, this is a second sizing as there is a sizing in makeStage
					sizeCanvas();
					// check to make sure any asset loading is done
					checkDispatch();
				}, 100);
				// for extra delay
				if (delay > 100) setTimeout(function() {
					sizeCanvas();
					checkDispatch();
				}, delay); // to catch delayed screen sizes
			} else {
				// check to make sure any asset loading is done
				checkDispatch();
			}

			// asset parameter
			that.loadAssetsCount = 0;
			that.assets = {}; // store asset Bitmap or play function for sound
			that.assetIDs = {}; // store ids for assets
			if (!zot(assets)) { // there are assets
				var q;
				if (assets.constructor == {}.constructor && !(assets.audioSprite || assets.id || assets.data || assets.src)) {
					q = that.loadAssets(zim.merge(assets, {progress:progress, queueOnly:true}));
				} else {
					q = that.loadAssets({assets:assets, path:path, progress:progress, queueOnly:true});
				}

				if (zot(q) || !q.on) {
					if (zon) zog("ZIM Frame - load failed");
					checkDispatch();
				} else {
					q.on("complete", function() {
						checkDispatch();
					});
				}
			} else {
				checkDispatch();
			}
		}

		function checkDispatch() {
			if (readyToDispatch) {
				if (!zot(progress) && progress.show) progress.hide();
				that.dispatchEvent("ready");
				appReady = true;
				dispatchResize();
			} else {
				readyToDispatch = true;
			}
		}

		function makeCanvas() {
			// note the width and height of a canvas
			// are separate from from the width and height styles
			// so beware of unintentionally stretching the canvas with styles

			var canvas = that.canvas = document.createElement("canvas");
			canvas.setAttribute("id", canvasID);
			canvas.setAttribute("tabindex", 0);
			var w = zim.windowWidth();
			var h = zim.windowHeight();
			var pRatio = window.devicePixelRatio || 1;
			// canvas.setAttribute("title", "application");
			if (scaling == "full" || scaling == "tag") {
				if (retina) {
					that.scale = pRatio;
					canvas.setAttribute("width", w*that.scale);
					canvas.setAttribute("height", h*that.scale);
					if (scaling == "full") {
						canvas.style.width = w+'px';
						canvas.style.height = h+'px';
					}
				} else {
					canvas.setAttribute("width", w);
					canvas.setAttribute("height", h);
				}
			} else {
				// if (retina) {
				// 	if (scaling == "fit") {
				// 		that.scale = (w/h >= stageW/stageH) ? h/stageH : w/stageW;
				// 	} else if (scaling == "outside") {
				// 		that.scale = (w/h >= stageW/stageH) ? w/stageW : h/stageH;
				// 	}
				// 	// that.scale *= pRatio;
				// 	canvas.setAttribute("width", stageW*that.scale);
				// 	canvas.setAttribute("height", stageH*that.scale);
				//
				// } else {
					canvas.setAttribute("width", stageW);
					canvas.setAttribute("height", stageH);
				// }
			}
			var alternative = zid(canvasID + "Alternative");
			if (alternative) canvas.appendChild(alternative);
			if (scaling == "tag" || scaling  == "inline") {
				tag.appendChild(canvas);
			} else {
				document.body.appendChild(canvas);
			}
			if (!zot(color)) canvas.style.backgroundColor = color;
			if (scaling == "full" || scaling == "fit" || scaling == "outside") {
				canvas.style.position = "absolute";
				if (!allowDefault) document.body.style.overflow = "hidden";
			}
		}

		function makeStage() {
			sizeCanvas();
			if (types.indexOf(scaling) != -1 && !allowDefault) {that.zil = zil();} // keep canvas still (from arrows, scrollwheel, etc.) (fit, outside and full only)
			stage = gpu?new zim.StageGL(canvasID, gpuObj):new zim.Stage(canvasID);
			if (!zot(color) && gpu) stage.setClearColor(zim.convertColor(color));
			stage.setBounds(0, 0, stageW, stageH);
			stage.width = stageW;
			stage.height = stageH;
			if (retina) sizeCanvas();
			if (rollover) stage.enableMouseOver(10); // if you need mouse rollover
			if (touch) createjs.Touch.enable(stage, false, allowDefault); // added for mobile
			if (allowDefault) stage.preventSelection = false; // thanks Jonghyun for the tip
			if (nextFrame) stage.nextStage = nextFrame.stage;
			if (nextStage) stage.nextStage = nextStage;
		}


		function sizeCanvas() {
			if (!that) return;
			var can = zid(canvasID);
			var w = zim.windowWidth();
			var h = zim.windowHeight();
			var newW; var newH;
			appOrientation = that.orientation = (w > h) ? "horizontal" : "vertical";
			if (appOrientation != lastOrientation) { // new orientation
				lastOrientation = appOrientation;
				that.dispatchEvent("orientation");
			}
			if (mobile && scrollTop) {setTimeout(function() {window.scrollTo(0, 0);}, 100);}
			if (!can) return;

			if (scaling == "fit") {
				// scales canvas to fit dimensions inside screen
				that.scale = (w/h >= stageW/stageH) ? h/stageH : w/stageW;
			} else if (scaling == "outside") {
				// scales canvas so screen inside dimensions
				that.scale = (w/h >= stageW/stageH) ? w/stageW : h/stageH;
			} else if (scaling == "full") {
				can.style.left = can.style.top = "0px";
				can.width = stageW = w;
				can.height = stageH = h;
				if (stage) {
					stage.setBounds(0,0,stageW,stageH); // need this
					if (retina) {
						var pRatio = window.devicePixelRatio || 1;
						can.width = w*pRatio;
						can.height = h*pRatio;
						can.style.width = w+'px';
						can.style.height = h+'px';
						that.scale = pRatio;
						if (stage) {
							stage.scaleX = stage.scaleY = that.scale;
							stage.tickOnUpdate = false;
							stage.update();
							stage.tickOnUpdate = true;
						}
					}
					stage.width = stageW;
					stage.height = stageH;
					if (gpu) stage.updateViewport(stageW, stageH);
				}
				zim.scrollX(0);
				zim.scrollY(0);
				setVisible();
				return;
			} else if (scaling == "tag") {
				// does not scale canvas but sets width and height to tag
				stageW = tag.style.width == "" ? (tag.getAttribute("width") || tag.offsetWidth) : tag.style.width;
				stageH = tag.style.height == "" ? (tag.getAttribute("height") || tag.offsetHeight) : tag.style.height;
				if (retina) {
					can.width = stageW*that.scale;
					can.height = stageH*that.scale;
					can.style.width = stageW+"px";
					can.style.height = stageH+"px";
					if (stage) {
						stage.scaleX = stage.scaleY = that.scale;
						stage.tickOnUpdate = false;
						stage.update();
						stage.tickOnUpdate = true;
					}
				} else {
					can.width = stageW;
					can.height = stageH;
				}
				if (stage) {
					stage.setBounds(0,0,stageW,stageH); // need this
					stage.width = stageW;
					stage.height = stageH;
					if (gpu) stage.updateViewport(stageW, stageH);
				}
				tag.style.overflow = "hidden";
				can.style.left = can.style.top = "0px";

				var bRect = can.getBoundingClientRect()
				that.x = bRect.x + zim.scrollX();
				that.y = bRect.y + zim.scrollY();
				setVisible();
				return;
			} else if (scaling == "inline") {
				// does not scale canvas but sets width and height
				if (retina) {
					var comp = window.getComputedStyle(can);
					var wid = zum(comp.getPropertyValue('width'));
					var hei = zum(comp.getPropertyValue('height'));
					var pRatio = window.devicePixelRatio || 1;
					that.scale = wid/width*pRatio;
					can.width = stageW*that.scale;
					can.height = stageH*that.scale;
					can.style.width = wid+'px';
					can.style.height = hei+'px';
					if (stage) {
						stage.scaleX = stage.scaleY = that.scale;
						stage.tickOnUpdate = false;
						stage.update();
						stage.tickOnUpdate = true;
					}
				}
				if (stage) {
					stage.setBounds(0,0,stageW,stageH); // need this
					stage.width = stageW;
					stage.height = stageH;
					if (gpu) stage.updateViewport(stageW, stageH);
				}
				can.style.left = can.style.top = "0px";
				var bRect = can.getBoundingClientRect()
				that.x = bRect.x + zim.scrollX();
				that.y = bRect.y + zim.scrollY();
				setVisible();
				return;
			}
			// scaling and positioning for fit and outside
			newH = stageH * that.scale;
			newW = stageW * that.scale;

			if (retina) {
				var pRatio = window.devicePixelRatio || 1;
				that.scale *= pRatio;
				can.width = stageW*that.scale;
				can.height = stageH*that.scale;
				can.style.width = (stageW*that.scale/pRatio)+'px';
				can.style.height = (stageH*that.scale/pRatio)+'px';
				if (stage) {
					stage.scaleX = stage.scaleY = that.scale;
					stage.tickOnUpdate = false;
					stage.update();
					stage.tickOnUpdate = true;
				}
			} else {
				can.style.width = newW + "px";
				can.style.height = newH + "px";
			}
			// note, changing the canvas width and height and scaling the stage
			// does not look as sharp at smaller scales - so decided to scale with styles
			// which is like scaling down an image
			// scaling up does not look as good - so just make your canvas as big as you will scale

			if (align=="left") that.x = 0;
			else if (align=="right") that.x = (w-newW);
			else that.x = ((w-newW)/2);
			// if (testText) testText.text = that.x;
			// if (that.stage) that.stage.update();
			if (valign=="top") that.y = 0;
			else if (valign=="bottom") that.y = (h-newH);
			else that.y = ((h-newH)/2);
			can.style.left = that.x + "px";
			can.style.top = that.y + "px";
			zim.scrollX(0);
			zim.scrollY(0);
			setVisible();
		}

		function setVisible() {
			if (scaling == "outside") {
				var beside = (zum(frame.canvas.style.width) - windowWidth()) / 2;
				if (align == "left") {
					that.visibleLeft = 0;
					that.visibleRight = windowWidth() / (windowWidth() + beside*2) * stageW;
				} else if (align == "right") {
					that.visibleLeft = beside*2 / (windowWidth() + beside*2) * stageW;
					that.visibleRight = stageW;
				} else {
					that.visibleLeft = beside / (windowWidth() + beside*2) * stageW;
					that.visibleRight = (windowWidth() + beside) / (windowWidth() + beside*2) * stageW;
				}
				var above = (zum(frame.canvas.style.height) - windowHeight()) / 2;
				if (valign == "top") {
					that.visibleTop = 0;
					that.visibleBottom = windowHeight() / (windowHeight() + above*2) * stageH;
				} else if (valign == "bottom") {
					that.visibleTop = above*2 / (windowHeight() + above*2) * stageH;
					that.visibleBottom = stageH;
				} else {
					that.visibleTop = above / (windowHeight() + above*2) * stageH;
					that.visibleBottom = (windowHeight() + above) / (windowHeight() + above*2) * stageH;
				}
			} else {
				that.visibleLeft = that.visibleTop = 0;
				that.visibleRight = stageW;
				that.visibleBottom = stageH;
			}
		}

		function dispatchResize() {
			if (!appReady || !that) return;
			that.dispatchEvent("resize");
			if ((!zim.OPTIMIZE&&(zns||!OPTIMIZE)) && stage && (scaling == "full" || scaling == "tag")) stage.update();
		}

		// ASSETS
		this.loadAssets = function(assets, path, progress, xhr, time, loadTimeout, outputAudioSprite, crossOrigin, fileType, queueOnly) {
			if (zot(assets)) return;
			if (zot(assets.src)) { // might be sending single parameter of asset object or audiosprite
				var sig = "assets, path, progress, xhr, time, loadTimeout, outputAudioSprite, crossOrigin, fileType, queueOnly";
				var duo; if (duo = zob(that.loadAssets, arguments, sig)) return duo;
			}
			if (!zot(progress) && progress.type == "ProgressBar" && zot(xhr)) xhr = true;
			if (!Array.isArray(assets)) assets = [assets];
			if (assets.length == 0) return;
			if (zot(time)) time = 0;
			if (zot(loadTimeout)) loadTimeout = 8000;
			if (zot(outputAudioSprite)) outputAudioSprite = false;
			if (zot(crossOrigin)) crossOrigin = "anonymous";
			if (zot(queueOnly)) queueOnly = false;

			var soundCheck = false;
			var manifest = [];
			var a; var ext; var i; var j;
			var re = /\.([^.]+)$/i; // get extension
			var fonts = [];
			var googleFonts = [];
			var nonFontCount = 0;
			var firstSoundCheck = true;

			for (i=0; i<assets.length; i++) {
				a = assets[i];
				if (a.constructor == {}.constructor) {
					if (a.audioSprite) {
						var obj = zim.copy(a);
						a = obj.src;
						prepareLoad(a);
						var replacement = [];
						var r;
						for (j=0; j<obj.audioSprite.length; j++) {
							r = obj.audioSprite[j];
							replacement.push({id:r[0], startTime:Math.round(r[1]*1000), duration:Math.round((r[2]-r[1])*1000)});
						}
						delete(obj.audioSprite); // the ZIM data
						obj.data = {audioSprite:replacement}; // the CreateJS data
						if (outputAudioSprite) zog(JSON.stringify(obj));
						manifest.push(obj);
					} else if (a.data && a.data.audioSprite) { // CreateJS AudioSprite
						var obj = zim.copy(a);
						a = obj.src;
						prepareLoad(a);
						manifest.push(obj);
					} else if (a.id) { // ZIM asset object
						that.assetIDs[a.id] = a.src;
						a = a.src;
						prepareLoad(a);
						var manObj = {src:a, loadTimeout:loadTimeout};
						if (!zot(fileType)) manObj.type = fileType;
						manifest.push(manObj);
					} else { // ZIM font object
						if (a.src.match(/fonts\.googleapis\.com/)) googleFonts.push(a);
						else fonts.push(a);
					}
				} else {
					prepareLoad(a);
					var manObj = {src:a, loadTimeout:loadTimeout};
					if (!zot(fileType)) manObj.type = fileType;
					manifest.push(manObj);
				}
				if (soundCheck && firstSoundCheck) {
					// dynamically make audio tag with sound to overcome some Apple devices not playing sounds
					var audioTag = document.createElement("audio");
					audioTag.setAttribute("src", (!zot(path)?path:"")+a);
					document.body.appendChild(audioTag);
					firstSoundCheck = false;
				}
			}
			function prepareLoad(a) {
				nonFontCount++;
				ext = a.match(re);
				if (ext && createjs.Sound.SUPPORTED_EXTENSIONS.indexOf(ext[1]) >= 0) soundCheck = true;
			}
			that.loadAssetsCount++;
			that.isLoading = true;
			var queue = new zim.Queue();
			queue.isLoading = true;
			queue.loadAssetsCount = 0;

            if (!zot(progress) && progress.show) {
				progress.zimActiveLoader = queue;
                progress.show();
            }

			if (fonts.length > 0) { // there are fonts to load
				// create manual list - which seems to always work
				var fontObjects = []
				var fontObject;
				var fontData;
				for (var i=0; i<fonts.length; i++) {
					fontData = fonts[i];
					fontObject = {
						src: "url("+(fontData.src.match(/^http/i)?"":(path?path:""))+fontData.src+")" + (fontData.type?" format('"+fontData.type+"')":""),
						family: fontData.font
					}
					if (fontData.weight) fontObject.weight = fontData.weight;
					if (fontData.style) fontObject.style = fontData.style;
					fontObjects.push(fontObject);
				}
				var fontLoader = new createjs.FontLoader({src:fontObjects}, true);
				fontLoader.on("complete", fontDone);
				fontLoader.on("error", fontDone);
			    fontLoader.load();
				queue.loadAssetsCount++;
			}
			if (googleFonts.length > 0) { // there are google fonts to load
				var fontLoader;
				for (var i=0; i<googleFonts.length; i++) {
					fontData = googleFonts[i];
					fontObject = {
						src: fontData.src,
						type: "fontcss"
					}
					queue.loadAssetsCount++; // each google font gets its own loader
					fontLoader = new createjs.FontLoader(fontObject, true);
					fontLoader.on("complete", fontDone);
					fontLoader.on("error", fontDone);
				    fontLoader.load();
				}
			}
			function fontDone() {
				queue.loadAssetsCount--;
				if (queue.loadAssetsCount == 0) endAssetLoad();
			}
			var startLoad = Date.now();
			var completeEventObject = new createjs.Event("complete");
			if (nonFontCount > 0) { // files other than fonts
				queue.loadAssetsCount++;
				var preload = queue.preload = that.preload = new createjs.LoadQueue(xhr, path, crossOrigin);
				if (soundCheck) preload.installPlugin(createjs.Sound);
				preload.on("progress", function(e) {queue.dispatchEvent(e); if (!queueOnly) that.dispatchEvent(e);});
				preload.on("error", function(e) {queue.dispatchEvent(e); if (!queueOnly) that.dispatchEvent(e);});
				preload.on("fileload", function(e) {
					var item = e.item;
					var type = e.item.type;
					var ext = item.id.match(re);
					var asset;
					if (type && type == "sound") {
						var soundIDs = [];
						if (item.data && item.data.audioSprite) {
							for (var i=0; i<item.data.audioSprite.length; i++) {
								soundIDs.push(item.data.audioSprite[i].id);
							}
						} else {
							soundIDs.push(item.id);
						}
						for (var i=0; i<soundIDs.length; i++) {
							~function() { // wrap soundID in a closure
								var soundID = soundIDs[i];
								asset = that.assets[soundIDs[i]] = {
			                        type:"sound",
									path:path,
			                        id:soundID,
			                        play:function(added) {
										if (added && added.loop===true) added.loop = -1;
										if (added && added.loopCount) added.loop = added.loopCount;
			                            var instance = createjs.Sound.play(soundID, added);
			                            instance.getStage = function(){return stage;}
			                            return instance;
			                        }
			                    };
							}();
						}
					} else if (type == "image") {
						asset = that.assets[item.id] = new zim.Bitmap(e.result, e.result.width, e.result.height, item.id);
					} else {
						asset = that.assets[item.id] = e.result;
					}
					var ev = new createjs.Event("assetload");
					ev.item = item; // createjs preload item
					ev.asset = asset;
					queue.dispatchEvent(ev);
					if (!queueOnly) that.dispatchEvent(ev);
				});
				that.preloadEvent = preload.on("complete", function(e) {
					completeEventObject = e;
					queue.loadAssetsCount--;
					if (queue.loadAssetsCount == 0) endAssetLoad();
				});
				try {preload.loadManifest(manifest)}catch(err){}
				// // strange - the try just stops an error but still loads
				// catch {
				// 	// setTimeout(function() {
				// 	// 	preload.loadManifest(manifest);
				// 	// }, 50);
				// }
			}

			function endAssetLoad() {
				// setting a time will force the preload to wait at least this amount of time
				// this can be used for testing or if you always want time to show a loading message
				var endLoad = Date.now();
				time = Math.max(0, time-(endLoad-startLoad));
				setTimeout(function() {
					that.loadAssetsCount--;
					if (that.loadAssetsCount <= 0) that.isLoading = false;
					queue.isLoading = false;
					queue.dispatchEvent(completeEventObject);
					if (!queueOnly) that.dispatchEvent(completeEventObject);
				}, time);
			}
			return queue;
		}

		this.asset = function(n) {
			if (zot(n)) return;
			var fromID = that.assetIDs[n];
			if (fromID) n = fromID;
			if (that.assets[n]) return that.assets[n];
			if (that.loadFailObj == "circles") var empty = frame.makeCircles(14);
			else var empty = that.loadFailObj;
			if (empty) {
				empty.type = "EmptyAsset";
				empty.id = n;
				empty.play = function(){if (zon) {zog("zim.Frame - asset("+n+") not found"); return {};}};
			}
			return empty;
		}

		Object.defineProperty(that, 'stage', {
			get: function() {
				return stage;
			},
			set: function(s) {
				zog("zim.Frame(): stage is read only - see remakeCanvas(), perhaps");
			}
		});

		Object.defineProperty(that, 'width', {
			get: function() {
				return stageW;
			},
			set: function(w) {
				zog("zim.Frame(): width is read only - see remakeCanvas(), perhaps");
			}
		});

		Object.defineProperty(that, 'height', {
			get: function() {
				return stageH;
			},
			set: function(h) {
				zog("zim.Frame(): height is read only - see remakeCanvas(), perhaps");
			}
		});

		Object.defineProperty(this, 'color', {
			get: function() {
				return color;
			},
			set: function(value) {
				color = value;
				if (!zot(value)) {
					zid(canvasID).style.backgroundColor = color;
					if (!zot(color) && gpu) stage.setClearColor(zim.convertColor(color));
				} else {
					zid(canvasID).style.backgroundColor = "default";
				}
			}
		});

		var _outerColor;
		Object.defineProperty(this, 'outerColor', {
			get: function() {
				return _outerColor;
			},
			set: function(value) {
				_outerColor = value;
				zet("body").css({backgroundColor:_outerColor});
			}
		});

		var _allowDefault = allowDefault;
		Object.defineProperty(that, 'allowDefault', {
			get: function() {
				return _allowDefault;
			},
			set: function(t) {
				if (t) {
					stage.preventSelection = false;
					document.body.style.overflow = "visible";
					if (that.zil) {
						window.removeEventListener("keydown", that.zil[0]);
						window.removeEventListener("wheel", that.zil[1]);
						window.removeEventListener("DOMMouseScroll", that.zil[2]);
						that.zil = null;
					}
				} else {
					stage.preventSelection = true;
					document.body.style.overflow = "hidden";
					if (zot(that.zil)) that.zil = zil();
				}
				_allowDefault = t;
			}
		});

		var eDown = new createjs.Event("keydown");
		this.eventRemove = eDown.remove;
		function keydownEvent(e) {
			e.remove = that.eventRemove;
			that.altKey = e.altKey;
			that.ctrlKey = e.ctrlKey;
			that.metaKey = e.metaKey;
			that.shiftKey = e.shiftKey;
			that.dispatchEvent(e);
		}
		window.addEventListener("keydown", keydownEvent);
		function keyupEvent(e) {
			that.altKey = e.altKey;
			that.ctrlKey = e.ctrlKey;
			that.metaKey = e.metaKey;
			that.shiftKey = e.shiftKey;
			e.remove = that.eventRemove;
			that.dispatchEvent(e);
		}
		window.addEventListener("keyup", keyupEvent);
			function wheelEvent(e) {
			that.dispatchEvent(e);
		}
		window.addEventListener("wheel", wheelEvent);
		function devicemotionEvent(e) {
			e.remove = that.eventRemove;
			that.dispatchEvent(e);
		}
		if (sensors && window.DeviceMotionEvent) {
			window.addEventListener("devicemotion",devicemotionEvent);
		}
		function deviceorientationEvent(e) {
			e.remove = that.eventRemove;
			// for some reason, reporting alpha as increasing going counter counterclockwise
			// so this code makes it increase going clockwise
			var z = 360-e.alpha;
			// compass is subtracting 180 if device screen is pointing down
			// in a VR helmet this would be looking slightly up from the horizon...
			// so removing this flip with the following code
			if (Math.abs(z-lastZ) > 180 - 45 && Math.abs(z-lastZ) < 180 + 45) flip = flip == 0 ? 180 : 0;
			lastZ = z;
			e.rotation = {x:e.beta, y:e.gamma, z:(z + flip) % 360};
			that.dispatchEvent(e);
		}
		if (sensors && window.DeviceOrientationEvent) {
			var lastZ = 0;
			var flip = 0;
			window.addEventListener("deviceorientation",deviceorientationEvent);
		}

		this.remakeCanvas = function(width, height) {
			if (scaling == "full") return;
			if (zot(width)) width = stageW;
			if (zot(height)) height = stageH;
			if (zid(canvasID)) zid(canvasID).parentNode.removeChild(zid(canvasID));
			stageW = width;
			stageH = height;
			makeCanvas();
			makeStage();
		}

		this.dispose = function() {
			window.removeEventListener('resize', windowResizeEvent);
			window.removeEventListener("keydown", keydownEvent); // thanks Reinout Mechant for the fix!
			window.removeEventListener("keyup", keyupEvent);
			window.removeEventListener("wheel", wheelEvent);
			window.removeEventListener("devicemotion",devicemotionEvent);
			window.removeEventListener("deviceorientation",deviceorientationEvent);
			recursiveDispose(stage);
			function recursiveDispose(obj) {
				obj.removeAllEventListeners();
				if (obj.numChildren) {
					for (var i=obj.numChildren-1; i>=0; i--) {
						recursiveDispose(obj.getChildAt(i));
					}
				}
				if (obj.parent) obj.parent.removeChild(obj);
			}
			if (zid(canvasID)) zid(canvasID).parentNode.removeChild(zid(canvasID));
			stage = null;
			that = null;
			return true;
		}

		// zim colors
		this.orange		= "#f58e25";
		this.green  	= "#acd241";
		this.pink  		= "#e472c4";
		this.blue   	= "#50c4b7";
		this.brown  	= "#d1a170";
		this.yellow   	= "#ebcb35";
		this.purple		= "#993399";
		this.red 		= "#fb4758"; // dedicated to Alexa
		this.silver		= "#999999";
		this.tin		= "#777777";
		this.grey   	= "#555555";
		this.gray 		= "#555555";
		this.lighter 	= "#eeeeee";
		this.moon 		= "#dddddd";
		this.light 		= "#cccccc";
		this.dark 		= "#333333";
		this.darker 	= "#111111";
		this.black 		= "#000000";
		this.white		= "#ffffff";
		this.clear 		= "rgba(0,0,0,0)";
		this.faint 		= "rgba(0,0,0,.01)";

		this.makeCircles = function(radius, multiple) {
			if (zot(radius)) radius = 100;
			var colors = [that.orange, that.green, that.pink, that.blue, that.brown, that.dark];
			if (multiple) {
				var c = new zim.Container({style:false});
				c.radius = radius;
				for (var i=0; i<colors.length; i++) {
					c.addChild(new zim.Circle((c.radius/colors.length)*(colors.length-i), colors[i], null, null, null, null, null, false));
				}
			} else {
				var c = new zim.Shape({style:false});
				var g = c.graphics;
				c.radius = radius;
				for (var i=0; i<colors.length; i++) {
					g.f(colors[i]).dc(0,0,(c.radius/colors.length)*(colors.length-i));
				}
				c.setBounds(-c.radius,-c.radius,c.radius*2,c.radius*2);
			}
			return c;
		}
	}
	zim.extend(zim.Frame, createjs.EventDispatcher, null, "cjsEventDispatcher", false);

	zim.Queue = function() {
		// internal usage only by Frame
		this.cjsEventDispatcher_constructor();
		this.isLoading = true; // thanks Frank Los for the suggestion.
	}
	zim.extend(zim.Queue, createjs.EventDispatcher, null, "cjsEventDispatcher");
	//-83


////////////////  ZIM META  //////////////

// the Meta section is for overall classes that operate on ZIM
// for instance zim.Distill and zim.Wonder

/*--
zim.DISTILL

distill
zim constant

DESCRIPTION
Distill allows you to track which functions you are using in your app
and create a custom minified js file with just those functions.
Set DISTILL to true to record which functions your are using in your app -
default is false.  While running your app, call the distill() function
take the results to https://zimjs.com/distill to create a minified distilled file.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// at the start of your code
DISTILL = true;

// at the end of your code (once everything has run)
// this means we may have to wait for events to happen, etc.
distill();

// this will log to the console a series of numbers
// separated by spaces representing the functions used

1 6 81 81 79 75 77 75 55 54 52 53 55 54 52 53 55 54 52
53 42 80 74 46 46 46 80 74 46 46 46 55 54 52 53 55 54
52 53 55 54 52 53 42 80 74

// copy these into the zim DISTILL form field
// to get the minified JavaScript for these functions
// NOTE: Distill will not duplicate the functions
// data duplication is left in for statistical purposes
END EXAMPLE
--*///+83.1
	var DISTILL = false;
	zim.distillery = [];
//-83.1

/*--
distill = function()

distill
zim function

DESCRIPTION
Call the distill function to display which zim functions you are using in your app.
You must set DISTILL constant to true before using (set at the start of your app).
After running through your app, call distill() and see the console (F12).
Take the results to https://zimjs.com/distill to create a minified distilled js file.
You would then host this js file yourself or include it in your mobile files, etc.
NOTE: distill() only records functions that have been used
so you may have functions still to be used in your app.
You will want to make sure you call distill() after you have used all your functions,
for instance, on a restart event, etc.
NOTE: distill() will not be available from your distilled file.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// at the start of your code
DISTILL = true;

// at the end of your code (once everything has run)
// this means we may have to wait for events to happen, etc.
distill();

// this will log to the console a series of numbers
// separated by spaces representing the functions used

1 6 81 81 79 75 77 75 55 54 52 53 55 54 52 53 55 54 52
53 42 80 74 46 46 46 80 74 46 46 46 55 54 52 53 55 54
52 53 55 54 52 53 42 80 74

// copy these into the zim DISTILL form field
// to get the minified JavaScript for these functions
// NOTE: Distill will not duplicate the functions
// data duplication is left in for statistical purposes
END EXAMPLE
--*///+83.2
	zim.distill = function() {
	 	if (!window.zns && zim && (zim.DISTILL || window.DISTILL)) {zim.distillery.push("83.3","83.35")} // zimplify runs before we can set zim.DISTILL
		zog("zim.distill() - go to https://zimjs.com/distill and enter the following:");
		zog((zim.distillery.length>0)?zim.distillery.join(" "):"must set zim.DISTILL = true;");
	}//-83.2

	//-83.1

/*--
zim.parseAudioSprite = function(audioSpriteData, outputAudioSprite)

parseAudioSprite
zim function

DESCRIPTION
Converts an AudioSprite data file from a popular AudioSprite Creation Tool: https://github.com/tonistiigi/audiosprite
To a CreateJS AudioSprite data format to use with Frame loadAssets().

NOTE: only gets the first file from the output and ignores loop and autoplay properties
To loop a sound, use frame.asset(id).play({loop:-1});

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// loading JSON data for audio is two step process.
// load the JSON then load the Audio with the converted loaded JSON data
// NOTE - do not have two frame complete events active at the same time!
// unless you assign the loadAssets() to a variable (ZIM Queue) and put the complete events on the variable
frame.loadAssets("audioSprite.json", "assets/");
frame.on("complete", init, null, true); // run this complete event only once (the true)

function init() {
	// convert imported data to CreateJS AudioSprite data:
	var audioSpriteData = parseAudioSprite(frame.asset("audioSprite.json"));

	// load the audio using the converted data
	frame.loadAssets(audioSpriteData, "assets/");
	frame.on("complete", function() {

		// now all the AudioSprite ids are available
		frame.asset("start").play();

	});
}
END EXAMPLE

PARAMETERS
audioSpriteData - an object (not JSON) made from parsed JSON data from https://github.com/tonistiigi/audiosprite example:
	{"resources": ["audiosprite.mp3"],
    "spritemap": {
        "blackball": {"start": 1.041, "end": 2.475, "loop": false},
	    "bounce": {"start": 3.567, "end": 4.232, "loop": false}
	}}
outputAudioSprite - (default false) output to the console the converted CreateJS AudioSprite data
	NOTE: this is in JSON format so will have to JSON.parse() the console text - wrap it in single quotes as they are stripped by the console

RETURNS - a CreateJS AudioSprite data object
--*///+83.25
	zim.parseAudioSprite = function(audioSpriteData, outputAudioSprite) {
	 	z_d("83.25");

		if (zot(audioSpriteData) || zot(audioSpriteData.resources) || zot(audioSpriteData.spritemap)) return;
		var obj = audioSpriteData;
		var id = audioSpriteData.resources[0];
		var data = audioSpriteData.spritemap;
		var newData = [];
		var r;
		var finalData = {src:id, data:{}};
		for (i in data) {
			r = data[i];
			newData.push({id:i, startTime:Math.round(r.start*1000), duration:Math.round((r.end-r.start)*1000)})
		}
		finalData.data.audioSprite = newData;
		if (outputAudioSprite) zog(JSON.stringify(finalData));
		return finalData;
	}//-83.25

/*--
zim.previewAudioSprite = function(audioSpriteData, numLetters, frame)

previewAudioSprite
zim function

DESCRIPTION
Creates a ZIM Tabs object with all the audioSprite sounds in each tab sound names are shortened to numLetters
Assumes that the AudioSprite is loaded - see docs on Frame.loadAssets()

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var zimAudioSpriteData = {
	src:"audiosprite.mp3",
	audioSprite:[
		// [id, startime(s), endtime(s)]
		// prefer this when making audioSprites by hand in Premiere or Audition
		['noStar', 1.041, 2.475],
		['bounce', 3.567, 4.232],
		['deleted', 5.396, 9.315],
		['click', 10.373, 10.499],
		['noLove', 11.607, 14.254],
		['happy', 15.672, 17.081],
		['draw', 18.354, 19.163],
		['heart', 20.151, 23.594],
		['star', 24.931, 27.673],
		['warning', 28.632, 29.351],
		['navChange', 30.640, 32.323]
	]
}
frame.loadAssets(zimAudioSpriteData, "assets/");
frame.on("complete", function() {
	// will show tabs at top of stage - press to play audio
	previewAudioSprite(zimAudioSpriteData, 2);
}
END EXAMPLE

PARAMETERS
audioSpriteData - an AudioSprite object of ZIM, CreateJS or from JSON parsed https://github.com/tonistiigi/audiosprite format
numLetters - (default 3) how many letters from the sound name to put on each tab
frame - (default zimDefaultFrame) the frame into which the AudioFile is loaded

RETURNS - a ZIM Tab which is automatically added to the frame's stage
--*///+83.26
	zim.previewAudioSprite = function(audioSpriteData, numLetters, frame) {
	 	z_d("83.26");

		if (zot(audioSpriteData)) return;
		if (zot(numLetters)) numLetters = 3;
		if (zot(frame)) {
			if (zot(zimDefaultFrame)) return;
			frame = zimDefaultFrame;
			stage = frame.stage;
		}

		var a = audioSpriteData;
		var tabs = [];
		var sounds = [];
		if (a.constructor == {}.constructor) {
			if (a.resources) a = zim.parseAudioSprite(a); // https://github.com/tonistiigi/audiosprite format
			if (a.audioSprite) { // ZIM format
				loop(a.audioSprite, function(asData) {
					tabs.push(asData[0].substr(0,numLetters));
					sounds.push(asData[0]);
				});
			} else if (a.data && a.data.audioSprite) { // CreateJS AudioSprite
				loop(a.data.audioSprite, function(asData) {
					tabs.push(asData.id.substr(0,numLetters));
					sounds.push(asData.id);
				});
			}
		} else {
			return;
		}
		var soundTab = new zim.Tabs({
			tabs:tabs, width:stageW, currentEnabled:true
		}).addTo(stage);
		soundTab.on("change", function() {
			frame.asset(sounds[soundTab.selectedIndex]).play();
		});
		return soundTab;

	}//-83.26

/*--
zim.svgToBitmap = function(svg, callBack)

svgToBitmap
zim function

DESCRIPTION
Function to convert SVG to ZIM Bitmap

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// svg can be a reference to an svg tag on the page zid("svgTagID")
// or an svg string starting with for example:
// var svg = '<svg id="vector" width="500" height="500" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 312 521" version="1.0">
// SEE: https://zimjs.com/svg for an example

svgToBitmap(svg, function(bitmap) {
	bitmap.center().transform();
	stage.update()
});
END EXAMPLE

PARAMETERS
svg - an SVG tag or SVG string
callBack - function to call when creation is done

RETURNS - null
--*///+83.27
	zim.svgToBitmap = function(svg, callBack) {
	 	z_d("83.27");

		if (!zot(svg.draggable)) {
			// CreateJS seems to wrap up an SVG with loadAssets as an SVG object
			var parser = new DOMParser();
			var svg = !svg.innerHTML?svg:parser.parseFromString(svg.innerHTML,"text/xml");
			var list = svg.getElementsByTagName("svg");
			var tag = list?svg.getElementsByTagName("svg")[0]:null;
			svg = tag;
		}

        if (!XMLSerializer) {if (zon) {zog("ZIM svgToBitmap() - sorry, not supported in Browser"); return;}}
        var svgString = (typeof svg == "string") ? svg : new XMLSerializer().serializeToString(svg);
        if (!svgString.match(/xmlns/i)) svgString = svgString.replace(/svg /i, "svg  xmlns='http://www.w3.org/2000/svg' ");
        var DOMURL = self.URL || self.webkitURL || self;
        var img = new Image();
        img.onload = function() {
            var bitmap = new zim.Bitmap(img);
            //timeout(1500, function() {
                // bitmap.cache(0,0,bitmap.width,bitmap.height);
                callBack(bitmap);
            //});
        };
        img.src = DOMURL.createObjectURL(new document.Blob([svgString], {type: "image/svg+xml;charset=utf-8"}));
    }//-83.27

	return zim;
} (zim || {});

// internal global function for the distill process
function z_d(n) {if (zim && (zim.DISTILL || window.DISTILL)) zim.distillery.push(n);}

// internal global function for adding DisplayMembers to zim Display Objects

/*--
zimify = function(obj, list)

zimify
global function

DESCRIPTION
Function to add display methods like drag, hitTests, move, animate, center, etc. to an object.
Also adds width, height, widthOnly and heightOnly properties.
The term "members" is used because we are adding both methods and properties.
All the ZIM 4TH display objects come with these members
BUT... the native CreateJS display objects do not.
When we import assets from Adobe Animate, these are native CreateJS objects.
So we can use zimify() to add these members to a CreateJS Shape, Container, etc.

NOTE: this was formerly zim.addDisplayMembers (which has been replaced by zimify)

ZIM uses zimify internally to add the members
to the ZIM shapes and components (Rectangle, Circle, Triangle, Label, Button, etc.)
as applied through the ZIM Container inheritance
as well as to the ZIM wrappers for CreateJS Container, Shape, Sprite, MovieClip objects.
The display methods call the original ZIM functions
passing the object parameter as the first parameter
or if DUO is being used then adds the object to the configuration object.

EXAMPLE
var cjsShape = new lib.Shape1(); // include the js from Adobe Animate
zimify(cjsShape);
cjsShape.center(stage);
cjsShape.drag();

// otherwise would have to use:
zim.center(cjsShape, stage);
zim.drag(cjsShape); // etc.
END EXAMPLE

EXAMPLE
var shape = new createjs.Shape();
shape.graphics.beginFill("red").drawRect(0,0,200,200);
shape.setBounds(0,0,200,200); // need to set bounds to be able to center
zimify(shape); // add methods like center, drag, etc.
shape.center(stage); // ZIM 4TH method format
stage.update();

// note: even without using zimify()
// we can use the traditional zim.center() function
var shape = new createjs.Shape();
shape.graphics.beginFill("red").drawRect(0,0,200,200);
shape.setBounds(0,0,200,200); // need to set bounds to center
zim.center(shape, stage); // use the zim function rather than the method
stage.update();

// of course we can just use a zim.Shape
// then the methods like center, drag, etc. are already added
var shape = new Shape(200, 200); // passing params sets bounds
shape.graphics.beginFill("red").drawRect(0,0,200,200);
shape.center(stage);
stage.update();

// in this case, we may have well used a Rectangle ;-)
var shape = new Rectangle(200, 200, "red");
shape.center(stage);
stage.update();
END EXAMPLE

PARAMETERS
obj - the object to add the methods and properties to (probably a CreateJS display object)
list - used internally by zimplify to exclude zim methods (makes zimify return list of methods)

RETURNS - obj for chaining
--*///+83.3

function zimify(obj, list) {
	z_d("83.3");

	var displayMethods = {
		drag:function(boundary, overCursor, dragCursor, all, swipe, localBounds, onTop, surround, slide, slideDamp, slideSnap, reg, removeTweens, startBounds, rect, currentTarget) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.drag(arguments[0]);}
			else {return zim.drag(this, boundary, overCursor, dragCursor, all, swipe, localBounds, onTop, surround, slide, slideDamp, slideSnap, reg, removeTweens, startBounds, rect, currentTarget);}
		},
		noDrag:function() {
			return zim.noDrag(this);
		},
		dragBoundary:function(boundary) {
			return zim.dragBoundary(this, boundary);
		},
		dragRect:function(boundary) {
			return zim.dragBoundary(this, boundary);
		},
		transform:function(move, stretchX, stretchY, scale, rotate, allowToggle, visible, onTop, showStretch, showRotate, showScale, showReg, showBorder, borderColor, borderWidth, dashed, customCursors, handleSize, regSize, snapDistance, snapRotation, cache, events, ghostColor, ghostWidth, ghostDashed, ghostHidden) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.transform(arguments[0]);}
			else {return zim.transform(this, move, stretchX, stretchY, scale, rotate, allowToggle, visible, onTop, showStretch, showRotate, showScale, showReg, showBorder, borderColor, borderWidth, dashed, customCursors, handleSize, regSize, snapDistance, snapRotation, cache, events, ghostColor, ghostWidth, ghostDashed, ghostHidden);}
		},
		setSwipe:function(swipe) {
			return zim.setSwipe(this, swipe);
		},
		gesture:function(move, scale, rotate, boundary, minScale, maxScale, snapRotate, localBounds, slide, slideEffect, regControl, onTop, surround, circularBounds, rect) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.gesture(arguments[0]);}
			else {return zim.gesture(this, move, scale, rotate, boundary, minScale, maxScale, snapRotate, localBounds, slide, slideEffect, regControl, onTop, surround, circularBounds, rect);}
		},
		noGesture:function(move, scale, rotate) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.noGesture(arguments[0]);}
			else {zim.noGesture(this, move, scale, rotate);}
		},
		gestureBoundary:function(boundary, update) {
			return zim.gestureBoundary(this, boundary, update);
		},
		gestureRect:function(boundary, update) {
			return zim.gestureBoundary(this, boundary, update);
		},
		addPhysics:function(dynamic, contract, shape, friction, linear, angular, density, restitution, maskBits, categoryBits, physics) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.addPhysics(arguments[0]);}
			else {return zim.addPhysics(this, dynamic, contract, shape, friction, linear, angular, density, restitution, maskBits, categoryBits, physics);}
		},
		removePhysics:function() {
			return zim.removePhysics(this);
		},
		hitTestPoint:function(x, y, boundsCheck) {
			return zim.hitTestPoint(this, x, y, boundsCheck);
		},
		hitTestReg:function(b, boundsCheck) {
			return zim.hitTestReg(this, b, boundsCheck);
		},
		hitTestRect:function(b, num, boundsCheck) {
			return zim.hitTestRect(this, b, num, boundsCheck);
		},
		hitTestCircle:function(b, num, boundsCheck) {
			return zim.hitTestCircle(this, b, num, boundsCheck);
		},
		hitTestCircles:function(b, margin) {
			return zim.hitTestCircles(this, b, margin);
		},
		hitTestBounds:function(b, margin, boundsShape) {
			return zim.hitTestBounds(this, b, margin, boundsShape);
		},
		boundsToGlobal:function(rect, flip, inside) {
			return zim.boundsToGlobal(this, rect, flip, inside);
		},
		resetBounds:function(a, b, c, d) {
			return zim.resetBounds(this, a, b, c, d);
		},
		hitTestPath:function(b, num, showPoints) {
			return zim.hitTestPath(this, b, num, showPoints);
		},
		hitTestGrid:function(width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type) {
			return zim.hitTestGrid(this, width, height, cols, rows, x, y, offsetX, offsetY, spacingX, spacingY, local, type);
		},
		animate:function(props, time, ease, call, params, wait, waitedCall, waitedParams, loop, loopCount, loopWait, loopCall, loopParams, loopWaitCall, loopWaitParams, rewind, rewindWait, rewindCall, rewindParams, rewindWaitCall, rewindWaitParams, sequence, sequenceCall, sequenceParams, sequenceReverse, ticker, cjsProps, css, protect, override, from, set, id, events, sequenceTarget, dynamic, drag, clamp, startPaused, clean, obj) {
			if (isDUO(arguments)) {arguments[0].target = this; return zim.animate(arguments[0]);}
			else {return zim.animate(this, props, time, ease, call, params, wait, waitedCall, waitedParams, loop, loopCount, loopWait, loopCall, loopParams, loopWaitCall, loopWaitParams, rewind, rewindWait, rewindCall, rewindParams, rewindWaitCall, rewindWaitParams, sequence, sequenceCall, sequenceParams, sequenceReverse, ticker, cjsProps, css, protect, override, from, set, id, events, sequenceTarget, dynamic, drag, clamp, startPaused, clean, obj);}
		},
		pauseAnimate:function(){return this;},
		stopAnimate:function(){return this;},
		wiggle:function(property, baseAmount, minAmount, maxAmount, minTime, maxTime, totalTime, type, ease, integer, id, startType, ticker) {
			if (isDUO(arguments)) {arguments[0].target = this; return zim.wiggle(arguments[0]);}
			else {return zim.wiggle(this, property, baseAmount, minAmount, maxAmount, minTime, maxTime, totalTime, type, ease, integer, id, startType, ticker);}
		},
		loop:function(call, reverse, step, start, end) {
			return zim.loop(this, call, reverse, step, start, end);
		},
		copyMatrix:function(source) {
			return zim.copyMatrix(this, source);
		},
		duplicate:function() {
			return zim.duplicate(this);
		},
		cur:function(type) {
			return zim.cur(this, type);
		},
		sha:function(color, offsetX, offsetY, blur) {
			return zim.sha(this, color, offsetX, offsetY, blur);
		},
		pos:function(x, y, right, bottom, container, index, add, reg, regX, regY) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.pos(arguments[0]);}
			else {return zim.pos(this, x, y, right, bottom, container, index, add, reg, regX, regY);}
		},
		loc:function(target, y, container, index, add, localToLocal, x) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.loc(arguments[0]);}
			else {return zim.loc(this, target, y, container, index, add, localToLocal, x);}
		},
		mov:function(x, y) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.mov(arguments[0]);}
			else {return zim.mov(this, x, y);}
		},
		top:function() {
			return zim.top(this);
		},
		bot:function() {
			return zim.bot(this);
		},
		ord:function(num) {
			return zim.ord(this, num);
		},
		dep:function(depth) {
			return zim.dep(this, depth);
		},
		alp:function(alpha) {
			return zim.alp(this, alpha);
		},
		hov:function(value, type) {
			return zim.hov(this, value, type);
		},
		rot:function(rotation, x, y) {
			return zim.rot(this, rotation, x, y);
		},
		siz:function(width, height, only) {
			return zim.siz(this, width, height, only);
		},
		ske:function(skewX, skewY) {
			return zim.ske(this, skewX, skewY);
		},
		reg:function(regX, regY, still) {
			return zim.reg(this, regX, regY, still);
		},
		sca:function(scale, scaleY) {
			return zim.sca(this, scale, scaleY);
		},
		scaleTo:function(boundObj, percentX, percentY, type, boundsOnly) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.scaleTo(arguments[0]);}
			else {return zim.scaleTo(this, boundObj, percentX, percentY, type, boundsOnly);}
		},
		fit:function(left, top, width, height, inside) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.fit(arguments[0]);}
			else {return zim.fit(this, left, top, width, height, inside);}
		},
		outline:function(color, size) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.outline(arguments[0]);}
			else {return zim.outline(this, color, size);}
		},
		addTo:function(container, index, localToLocal) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.addTo(arguments[0]);}
			else {return zim.addTo(this, container, index, localToLocal);}
		},
		removeFrom:function(container) {
			return zim.removeFrom(this, container);
		},
		added:function(call, interval, maxTime) {
			return zim.added(this, call, interval, maxTime);
		},
		tap:function(call, distance, time, once) {
			return zim.tap(this, call, distance, time, once);
		},
		noTap:function() {
			return zim.noTap(this);
		},
		change:function(call, once) {
			return zim.change(this, call, once);
		},
		noChange:function() {
			return zim.noChange(this);
		},
		centerReg:function(container, index, add) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.centerReg(arguments[0]);}
			else {return zim.centerReg(this, container, index, add);}
		},
		center:function(container, index, add) {
			if (isDUO(arguments)) {arguments[0].obj = this; return zim.center(arguments[0]);}
			else {return zim.center(this, container, index, add);}
		},
		place:function(id) {
			return zim.place(this, id);
		},
		placeReg:function(id) {
			return zim.placeReg(this, id);
		},
		expand:function(padding, paddingVertical) {
			return zim.expand(this, padding, paddingVertical);
		},
		setMask:function(mask, dynamic) {
			return zim.setMask(this, mask, dynamic);
		},
		cloneProps:function(clone) { // from CreateJS DisplayObject
			clone.type = this.type;
			clone.group = this.group;
			clone.style = this.style;
			clone.alpha = this.alpha;
			clone.rotation = this.rotation;
			clone.mouseEnabled = this.mouseEnabled;
			clone.tickEnabled = this.tickEnabled;
			clone.x = this.x;
			clone.y = this.y;
			clone.scaleX = this.scaleX;
			clone.scaleY = this.scaleY;
			clone.skewX = this.skewX;
			clone.skewY = this.skewY;
			clone.name = this.name;
			clone.regX = this.regX*clone.width/this.width;
			clone.regY = this.regY*clone.height/this.height;
			clone.visible = this.visible;
			clone.shadow = this.shadow;
			if (!this.type || this.type=="Shape") zim.copyMatrix(clone, this);
			clone.compositeOperation = this.compositeOperation;
			clone.snapToPixel = this.snapToPixel;
			clone.filters = this.filters==null?null:this.filters.slice(0);
			clone.mask = this.mask;
			clone.hitArea = this.hitArea;
			// clone.cursor = this.cursor;
			// clone._bounds = zim.copy(this._bounds); // can't copy as _bounds is not an {} or []
			if ((!this.type || this.type=="Shape") && this._bounds) clone.setBounds(this._bounds.x, this._bounds.y, this._bounds.width, this._bounds.height);
			return clone;
		},
		cloneChildren:function(clone) {
			if (clone.children.length) clone.removeAllChildren();
			var arr = clone.children;
			for (var i=0, l=this.children.length; i<l; i++) {
				var childClone = this.children[i].clone();
				childClone.parent = clone;
				arr.push(childClone);
			}
			return clone;
		}
	}
	if (!zot(list)) {
		var list = []
		for (var m in displayMethods) {
			list.push(m);
		}
		return list;
	}
	for (var i in displayMethods) {
		if (displayMethods.hasOwnProperty(i)) {
			obj[i] = displayMethods[i];
		}
	}
	Object.defineProperty(obj, 'width', {
		enumerable: true,
		get: function() {
			// that.setBounds(null);
			var b = this.getBounds();
			return (zot(b))?null:Math.abs(b.width*this.scaleX);
		},
		set: function(value) {
			var b = this.getBounds();
			if (zot(b) || b.width==0) {
				if (zon) zog("width needs bounds set with setBounds()");
				return;
			}
			var s = value/b.width;
			this.scaleX = this.scaleY = s;
		}
	});
	Object.defineProperty(obj, 'height', {
		enumerable: true,
		get: function() {
			// that.setBounds(null);
			var b = this.getBounds();
			return (zot(b))?null:Math.abs(b.height*this.scaleY);
		},
		set: function(value) {
			var b = this.getBounds();
			if (zot(b) || b.height==0) {
				if (zon) zog("height needs bounds set with setBounds()");
				return;
			}
			var s = value/b.height;
			this.scaleX = this.scaleY = s;
		}
	});
	Object.defineProperty(obj, 'widthOnly', {
		enumerable: true,
		get: function() {
			// that.setBounds(null);
			var b = this.getBounds();
			return (zot(b))?null:Math.abs(b.width*this.scaleX);
		},
		set: function(value) {
			var b = this.getBounds();
			if (zot(b) || b.width==0) {zog("widthOnly needs bounds set with setBounds()"); return;}
			var s = value/b.width;
			this.scaleX = s;
		}
	});
	Object.defineProperty(obj, 'heightOnly', {
		enumerable: true,
		get: function() {
			// that.setBounds(null);
			var b = this.getBounds();
			return (zot(b))?null:Math.abs(b.height*this.scaleY);
		},
		set: function(value) {
			var b = this.getBounds();
			if (zot(b) || b.height==0) {zog("heightOnly needs bounds set with setBounds()"); return;}
			var s = value/b.height;
			this.scaleY = s;
		}
	});
	Object.defineProperty(obj, 'depth', {
		enumerable: true,
		get: function() {
			return this._depth;
		},
		set: function(depth) {
			this._depth = depth;
			if (!zot(this.vrChannel)) {
				var parallaxShift = this.vrParallaxDistance ? depth*this.vrParallax*this.vrParallaxDistance : 0;
				if (this.vrChannel == "left") {
					this.x = this.vrStartX + depth + parallaxShift;
				} else if (this.vrChannel == "right") {
					this.x = this.vrStartX - depth + parallaxShift;
				}
			}
		}
	});
	Object.defineProperty(obj, 'blendMode', {
		enumerable: true,
		get: function() {
			return this.compositeOperation;
		},
		set: function(value) {
			this.compositeOperation = value;
		}
	});
	return obj;

}//-83.3

/*--
zimplify = function(exclude)

zimplify
global function

DESCRIPTION
Removes requirement to use the zim namespace in code
Puts the ZIM code, display and controls into the global namespace
Does not put methods in the namespace as using methods as functions is discouraged
With the exception of loop, stopAnimate, pauseAnimate, animate, wiggle

NOTE: JavaScript Window class is stored as document.Window
and JavaScript Blob class is stored as document.Blob to provide access as these are overwritten by zimplify
Alternatively, zns can be set to true then run zimplify(["Window", "Blob"]);
at which point the zim namespace would be needed for a ZIM Window and ZIM Blob but the others would need no namespace
and the JavaScript Window and Blob would be available as Window and Blob.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var circle = new Circle(50, green);
// was:
var circle = new zim.Circle(50, green);

var random = rand(500);

// note - do not use:
var rand = rand(500); // as you will overwrite the rand() reference
END EXAMPLE

PARAMETERS
exclude - (default null) a String command or an array of command strings to not remove the zim namespace

--*///+83.35
var ignore;
function zimplify(exclude) {
	z_d("83.35");


	document.window = Window;
	document.window = window;
	document.Blob = Blob;
	ignore = "ignore";
	if (zot(exclude)) exclude = [];
	if (!Array.isArray(exclude)) exclude = [exclude];
	var methods = zimify(null, true); // get list of zim methods
	var exceptions = ["loop", "stopAnimate", "pauseAnimate", "animate", "wiggle"]
	for (var command in zim) {
		if ((methods.indexOf(command) == -1 || exceptions.indexOf(command) >= 0) && exclude.indexOf(command) == -1) {
			window[command] = zim[command];
		}
	}
	window["KEYFOCUS"] = zim.KEYFOCUS;
	window["OPTIMIZE"] = zim.OPTIMIZE;
	window["ACTIONEVENT"] = zim.ACTIONEVENT;
	window["STYLE"] = zim.STYLE;
}//-83.35


/*--
ZIMONON

ZIMONON (note the extra ON at the end of ZIMON - so ZIMONON)
ZIM constant

*** this just turns ZIMON on - see docs for ZIMON next!

DESCRIPTION
Default is false - set to true to record arguments needed for ZIMON.stringify()
If not using ZIMON.stringify() then do not set this to true
See also Docs for Static Class ZIMON

BACKGROUND
Arguments are the values passed in to parameters.
These are stored in a LOCAL variable called arguments that belongs to the object
For ZIMON to stringify an object, it needs to have access to the arguments
So ZIMON requires objects to keep a PUBLIC record in an arguments property
This can add up in memory when the app has hundreds or thousands of objects
For instance, every rectangle, circle and line in a blob control would keep a record of its arguments

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
ZIMONON = true
var frame = new Frame();
frame.on("ready", function () {

	var circle = new Circle(100, blue);
	var c = ZIMON.stringify(circle);
	// store string c in localStorage or send to database
	// ZIMON would only make a default circle if ZIMONON were not true

	// we can parse a ZIMON string without needing ZIMONON set to true
	// so if the app only read in a ZIMON string
	// then there would be no reason to set ZIMONON to true
	var cc = ZIMON.parse(cc).center(); // centers a blue circle

	stage.update();
})
END EXAMPLE
--*///+83.37
var ZIMONON = false;
//-83.37



// back into zim
var zim = function(zim) {

/*--
ZIMON = {}

ZIMON
zim static class

DESCRIPTION
ZIMON stands for ZIM Object Notation
It turns any prepared** object to a string using ZIMON.stringify(object)
It turns a ZIMON string back into and object using ZIMON.parse(string)
The object can be inside an array [] or an object literal {}
These can be nested and filled with any type of object (that is prepared**)
See https://zimjs.com/zimon/ for an example

NOTE: must set ZIMONON=true at START of app to record arguments (note the extra ON)
it is not that much extra data but it is prudent to conserve memory when not using ZIMON
ZIMONON is needed only to stringify(), for parse() it does not matter

KEY
An optional key object can be used to specify the scope of the class
and to specify which properties to store for objects made from a class.
The scope must be a string accessible from where you stringify - it can contain dots (.)
The key can be passed as the second parameter to ZIMON.stringify().
The key is sent in the ZIMON string and nothing extra is needed when parsing.

PURPOSE
The purpose is similar to JSON - to save to localStorage or databases or share between languages.
Indeed, the final ZIMOM format is JSON.
Like JSON, ZIMON can be used outside of ZIM and outside JavaScript if implemented in another language.
See https://github.com/danzen/zimon/ for generic JS code to port to other languages.

** PREPARED OBJECTS
Any objects NOT supported by JSON must be prepared
so objects other than string, number, array, object literal, boolean or null
need to have the following:
1. A type property that matches the class name as a string
2. An arguments property that holds an array of the arguments passed to make the object
The ZIM Objects are already prepared - so no extra work is required

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var objects = [new Rectangle(100,100,red), new Circle(50, blue)];
var zimon = ZIMON.stringify(objects); // could save in localStorage or send to database

var newObjects = ZIMON.parse(zimon); // makes a Rectangle and Circle in a []
newObjects[0].center();
newObjects[1].center().animate({scale:.5}, 1000);
zog(newObjects[0].color); // #fb4758 zim red
END EXAMPLE

EXAMPLE
// pass a key for any properties to remember
var objects = [new Rectangle(100,100,red).alp(.5), new Circle(50, blue).sca(2).loc({x:200,y:200,add:false})];
var key = {Rectangle:{props:["alpha"]}, Circle:{props:["scale", "x", "y"]}};
var zimon = ZIMON.stringify(objects, key); // could save in localStorage or send to database

var newObjects = ZIMON.parse(zimon); // makes a Rectangle and Circle in a []
newObjects[0].center(); // would have alpha .5
newObjects[1].addTo().animate({scale:.5, x:stageW/2, y:stageH/2}, 1000); // would start at scale 2 and x,y of 100,100
END EXAMPLE

EXAMPLE
// To prepare custom objects:
Custom = function(a, b, c) { // ** global scope
	this.type = "Custom"; // ZIMON uses a type property that matches the class name
	// best to take copy of arguments and turn then into array
	// otherwise any changes to argument values (setting defaults, etc) will be needlessly recorded
	// when ZIMON.stringify() is eventually called
	this.arguments = Array.prototype.slice.call(arguments);
	this.write = function() {console.log(a, b, c);}
}
var c = new Custom("one", "two", "three");
var c2 = ZIMON.stringify(c);
var c3 = ZIMON.parse(c2);
c3.write(); // logs one two three

// ** or save in some namespace or on an object
frame.Custom = function(a, b, c) {// same as above}
var c = new frame.Custom("one", "two", "three");
// then use key as second parameter to specify scope
var c2 = ZIMON.stringify(c, {Custom:{scope:"frame"}}); // key
var c3 = ZIMON.parse(c2);
c3.write(); // logs one two three
END EXAMPLE

EXAMPLE
// a built-in JavaScript Object:
var date = new Date(1982, 0, 10);
date.type = "Date"; // add date
date.arguments = [1982, 0, 10]; // and manual arguments
var d = ZIMON.stringify(date);
var d2 = ZIMON.parse(d);
zog(d2.getYear()); // 82
END EXAMPLE


STATIC METHODS
stringify(obj, key) - supply an object to convert to a ZIMON string
	the object can be any JSON ready object - string, number, array, object literal, boolean or null
	the object can be any ZIM DisplayObject - Circle, Blob, Button, Tile, Emitter, etc.
	the object can be a prepared custom or built in object
		with a type property of the class name
		with an arguments property of the arguments - see ** PREPARED OBJECTS above
	Or any combination of these - ZIMON is recursive so checks parameters, etc.
	the optional key can be used to specify scope for the class if not in global scope
	and used to specify which properties should be recorded
	the key has the following format:
	 	{ClassName:{scope:"inScope.nested.etc", props:["prop1", "prop2", "etc"]}, ClassName2:{}, etc.}
	stringify returns a ZIMON string in JSON format
	eg.'{"obj":{"zimon":"Rectangle","args":[30,30,"#e472c4"]},"props":[0.3,212,184]},"key":{"Tile":{"props":["alpha","x","y"]}},"zimon":1,"info":"https://zimjs.com/zimon/"}';
parse(string) - pass in the ZIMON string to turn back into an object (or objects within objects)


--*///+83.39
zim.ZIMON = {
	stringify:function(obj, key) {
		z_d("83.39");
		// Assumes all non-JSON objects have a type property matching its class name.
		// If the class is not in the global scope then a key object must be used:
		// key = {scope:"zim"}; // where zim is in the global scope
		// the string value can contain dots for nested scope {scope:"zim.game"}
		// If different functions have different scopes use a specific scope:
		// key = {Person:{scope:"zim"}, Orb:{scope:"zim.game"}};
		// or key = {scope:"zim", Person:{scope:"zim"}};
		// Use a props property to transfer property values from old to new objects
		// key = {Person:{scope:"zim", props:["rotation", "alpha"]}};

		if (zot(obj)) return;
		if (!ZIMONON) {
			if (zon) zog("ZIMON - set ZIMONON=true before using ZIMON.stringify()");
		 	return;
		}
		obj = zim.copy(obj);
		process(obj);
		return JSON.stringify({obj:obj, key:key, zimon:1, info:"https://zimjs.com/zimon/"});
		function process(data, parent, index) {
			if (zot(data)) return;
			if (Array.isArray(data)) {
				zim.loop(data, function (d, i) {
					process(d, data, i);
				});
			} else if (data.constructor == {}.constructor) {
				zim.loop(data, function (p,v) {
					process(v, data, p);
				});
			} else {
				if (data.type) {
					var proxy = {zimon:data.type};
					if (data.arguments) {
						proxy.args = Array.prototype.slice.call(data.arguments);
						process(proxy.args);
					}
					if (data.type == "series") {
						proxy.args = Array.prototype.slice.call(data.array);
						process(proxy.args);
					}
					if (key && key[data.type] && key[data.type].props) {
						proxy.props = [];
						zim.loop(key[data.type].props, function (p) {
							proxy.props.push(data[p]);
						});
					}
					if (parent) {
						parent[index] = proxy;
					} else {
						obj = proxy;
					}
				} else {
					if (
						typeof data != "string" &&
						typeof data != "number" &&
						typeof data != "boolean" &&
						data !== null
					) {
						zog("ZIMON - potentially bad JSON value" + data);
					}
				}
			}
		}
	},
	parse:function(string, key) {
		z_d("83.39");
		if (zot(string)) return;
		var o = JSON.parse(string);
		var obj = o.obj; // could use ES6 deconstruct
		var key = o.key;
		process(obj);
		return obj;
		function process(data, parent, index) {
			if (zot(data)) return;
			if (Array.isArray(data)) {
				zim.loop(data, function (d, i) {
					process(d, data, i);
				});
			} else if (zot(data.zimon) && data.constructor == {}.constructor) {
				zim.loop(data, function (p,v) {
					process(v, data, p);
				});
			} else {
				if (data.zimon) {
					// turn the zimon format into an object using scope in key
					var cl = data.zimon; // the class name

					function translate(n) {
						try {
							return Function('return('+n+')')();
						} catch (err) {
							if (zon) zog("ZIMON: bad function scope or name: " + n);
							return null;
						}
					}
					var scope = "";
                    if (key) {
                    	if (key[cl] && key[cl].scope) {
                    		scope = key[cl].scope+".";
                    	} else if (key.scope) {
                    		scope = key.scope+".";
                    	}
                    }
					var fn = translate(scope+cl);

					var args = data.args;
					if (zot(args)) args = [];
					process(args);
					if (data.type == "series") {
						var z = zim.series(args);
					} else {
						var z = !fn?null:new (fn.bind.apply(fn,[null].concat(Array.prototype.slice.call(args))))();
					}
					if (parent) {
						parent[index] = z;
					} else {
						obj = z;
					}
					if (key && key[cl] && key[cl].props) {
						if (z) {
							zim.loop(key[cl].props, function (p, i) {
								if (!zot(data.props[i])) z[p] = data.props[i];
							});
						}
					}
				}
			}
		}
	}
}//-83.39

/*--
zim.Wonder = function(wid, client, app, notes, server)

Wonder
zim class

DESCRIPTION
Wonder sends counts, times, and orders to a server for user testing or statistical purposes.
Go to http://zimjs.com/wonder/ to get a Wonder ID (wid) and set up Wonder stats with ZIM
or make up your own wid and use your own server script to collect data.
See the zim Wonder site for a sample script to collect data.
NOTE: all records at ZIM are archived NEW YEARS DAY and kept for a year after that.
Service is provided as is and ZIM and Dan Zen are not responsible for lost data.

USAGE
count will count things like app loads, button clicks within an app, how many monsters they killed
time will tell you the time the user took to do something - like solve a puzzle, or locate the witch
order will record the order items were done - which section did they go to first, second, third, etc.

NOTE: as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
// make a Wonder object
// wonderID is e-mailed to you when you sign up
// client is your client's name that you provide
// app is the app for which you are recording data
// you can also pass an optional note
var wonder = new Wonder("wonderID", "client", "app");

// COUNT EXAMPLE
// for this example we count times a button is pressed
var button = new Button("CLICK");
button.center(stage);
button.on("click", function(){
	// records an entry for this keyword in your stats
	// along with date, time, session, etc.
	wonder.count("wow");
});

// TIME EXAMPLE
// assuming we have our Wonder object from above
// (you only need one Wonder object)
// start the timer counting for a keyword called "test"
// this will record nothing until you timeEnd()
// or you timeStart() again
// you can also timePause() and timeUnpause()
// see DOCS for more functionality and information
wonder.timeStart("test");

// add the circle
var circle = new Circle(100, "red");
circle.center(stage);
circle.drag();
circle.on("pressup", function(){
	if (circle.hitTestRect(square)) {
		// if the shapes are hitting then end the timer
		// this will send data to your Wonder report
		wonder.timeEnd("test");
	}
});

// add the square to a random location on stage
var square = new Rectangle(100, "yellow");
stage.addChild(square);
square.x = rand(stageW-square.width);
square.y = rand(stageH-square.height);

// ORDER EXAMPLE
// assuming we have our Wonder object from above
// (you only need one Wonder object)

// make tabs
var tabs = new Tabs(400, 40, ["MOUSE", "CAT", "MONKEY"]);
tabs.selectedIndex = -1; // start with no selection
tabs.center(stage);
var count = 0; // perhaps get the first four presses
tabs.on("change", function(){
	// record which tab was pressed
	// this gets stored under keyword animal
	wonder.order("animal", tabs.text);
	count++;
	// turn the order recording off for "animal"
	if (count == 4) wonder.orderOff("animal");
});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
wid - string with your company wonder ID for example z14i46m3z29
	  this is the ID you are e-mailed when you sign up or sign in with your company name
	  this is NOT your company name that you log into Wonder with
	  NOTE: recording to a non-registered wid on the ZIM server will not work and there is no error message
client - the client the app is for - if it is for your company, just put your company
app - the app or site the Wonder stats are for
notes - (default null) any extra notes like any user data (limit 256 characters as it is stored each record)
server - a server with zim Wonder running
	Note: the default value for the server parameter has been removed as it risks being out-of-date
	If you have signed up for ZIM Wonder at https://zimjs.com/wonder/ then
	import http://d309knd7es5f10.cloudfront.net/zimserver_url.js in your code (script tag up top)
	this gives a global zimWonderURL variable to pass into the server parameter
	the zimserver_url.js script will always hold the latest domain:port for the zim server

METHODS
count(keyword) - sends a line to the server script with the given keyword as well as date and time
timeStart(keyword) - starts timing for the specified keyword (nothing sent to server yet)
timePause(keyword) - pauses the timing for this keyword
timeUnpause(keyword) - unpauses the timing for this keyword
timeEnd(keyword) - ends timing for the specific keyword and sends the time to the server
	NOTE: if the user exits the app (or leaves page) nothing gets sent to the server
		  due to unreliable beforeUnload events in the HTML world (otherwise all this would be batched)
order(keyword, item) - sends a line to the server for this item along with a unique order id for the keyword for the user

countOff(keyword) - prevents counts from being sent for this keyword
countOn(keyword) - allows counts from being sent for this keyword (default)
timeOff(keyword) - prevents sending time to the server for this keyword
timeOn(keyword) - allows sending time to the server for this keyword (default)
orderOff(keyword) - prevents sending orders to the server for this keyword
orderOn(keyword) - allows sending orders for this keyword (default)

dispose() - clear any event listeners, etc.
--*///+82
	zim.Wonder = function(wid, client, app, notes, server) {
		var sig = "wid, client, app, notes, server";
		var duo; if (duo = zob(zim.Wonder, arguments, sig, this)) return duo;
		z_d("82");
		if (zot(wid)) {zog("zim.Wonder() - please provide Wonder ID (see https://zimjs.com/wonder/)"); return;}
		if (zot(server)) server = "http://54.237.229.197:3001/wonder"; // adjust to amazon server
		var that = this;
		if (zot(zim.wonderSession)) zim.wonderSession = "W"+zim.rand(100000,999999); // session id
		var data = [];
		// buffer to send at most every second
		var wonderInterval = setInterval(sendData,1000);
		var sendCount = 0;
		function sendData() {
			if (data.length > 0) {
				zim.async(server + "?wonder=" + JSON.stringify(data));
				data = [];
				sendCount++;
			}
		}
		var lastKeyword;
		var wonderCheck = setInterval(function(){
			if (sendCount > 28) {
				data.push({id:wid, c:client, a:app, n:notes, k:lastKeyword, t:"e", v:"frequency max - terminated", s:zim.wonderSession});
				zog("zim.Wonder() - frequency max - terminated");
				that.dispose();
			}
			sendCount=0;
		}, 30*1000); // 30 seconds
		this.countsOff = {};
		this.timesOff = {};
		this.ordersOff = {};
		function kw(k,t){
			if (zot(k)) {
				zog("zim.Wonder "+t+" - please provide a keyword"); return false;
			} else { // check if in off lists
				if (that[t+"sOff"][k]) return false;
				return true;
			}
		}
		this.count = function(keyword) {
			if (!kw(keyword, "count")) return;
			lastKeyword = keyword;
			data.push({id:wid, c:client, a:app, n:notes, k:keyword, t:"c", v:1, s:zim.wonderSession});
		}
		var times = {};
		this.timeStart = function(keyword) {
			if (!kw(keyword, "time")) return;
			that.timeEnd(keyword);
			lastKeyword = keyword;
			times[keyword] = new Date().getTime();
		}
		var pauseTimes = {};
		this.timePause = function(keyword) {
			if (!kw(keyword, "time")) return;
			if (pauseTimes[keyword]) return; // already pausing
			pauseTimes[keyword] = new Date().getTime();
		}
		this.timeUnpause = function(keyword) {
			if (!kw(keyword, "time")) return;
			if (!pauseTimes[keyword]) return; // no pauses
			var pausedTime = new Date().getTime() - pauseTimes[keyword];
			if (times[keyword]) times[keyword] += pausedTime;
			delete pauseTimes[keyword];
		}
		this.timeEnd = function(keyword) {
			if (!kw(keyword, "time")) return;
			if (!times[keyword]) return;
			var t1 = (pauseTimes[keyword]) ? pauseTimes[keyword] : new Date().getTime();
			var time = Math.round((t1 - times[keyword])/1000);
			delete pauseTimes[keyword];
			delete times[keyword];
			data.push({id:wid, c:client, a:app, n:notes, k:keyword, t:"t", v:time, s:zim.wonderSession});
		}
		this.order = function(keyword, item) {
			if (!kw(keyword, "order")) return;
			lastKeyword = keyword;
			if (zot(item)) {zog("zim.Wonder order() - please provide an item"); return;}
			data.push({id:wid, c:client, a:app, n:notes, k:keyword, t:"o", v:item, s:zim.wonderSession});
		}
		this.countOff = function(keyword) {that.countsOff[keyword] = 1;}
		this.countOn = function(keyword) {delete that.countOff[keyword];}
		this.timeOff = function(keyword) {that.timesOff[keyword] = 1;}
		this.timeOn = function(keyword) {delete that.timesOff[keyword];}
		this.orderOff = function(keyword) {that.ordersOff[keyword] = 1;}
		this.orderOn = function(keyword) {delete that.ordersOff[keyword];}

		this.dispose = function() {
			sendData();
			clearInterval(wonderInterval);
			clearInterval(wonderCheck);
			return true;
		}
	}//-82


////////////////  ZIM GAME  //////////////

// the Game section is for the game helper library

/*--
zim.LeaderBoard = function(data, title, width, height, corner, backgroundColor, titleColor, colors, total, scoreWidth, scorePlaces, scoreZeros, spacing, arrows, borderColor, borderWidth, shadowColor, shadowBlur, reverse, allowZero, font, fontSize, nameShift, scoreShift, rankShift)

LeaderBoard
zim class - extends a zim.Container which extends a createjs.Container
** MUST import game module - SEE link at top of Docs for GAME under ZIM HELPER modules

DESCRIPTION
Shows leaders in a game.  Allows player to enter initials if in top players as set by the total parameter (default 10)
For the data parameter you can set an ID for ZIM data storage - get the ID at http://zimjs.com/leaderboard/
Or you can set the data parameter to "localStorage" for device only or "manual" for data from a custom database
See the ZIM ZONG game example for putting a LeaderBoard in a zim.Pane http://zimjs.com/zong/

NOTE as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var lb = new LeaderBoard({
	data:"E-MAILED CODE FROM zimjs.com/leaderboard/",
	corner:0,
	backgroundColor:dark
}).center();

// then to record a score at some point later:
lb.score(500);
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
data - (default "localStorage") localStorage will let a user keep their high score on a computer
	set data to an ID that is provided at http://zimjs.com/leaderboard/ to use the free ZIM data service
	set data to an Array of score objects: [{player:"RAJ", score:20}, {player:"DAN", score:18}] for manual database usage
title - (default null) - set to a String to create a title above the board
width - (default 400) - the width of the board
height - (default 600) - the height of the board
corner - (default 20) - the rounded corner of the board - set to 0 for no corner
backgroundColor - (default "white") - the backing color of the board
titleColor - (default "#222") - the title text color if a title is provided
colors - (default the object detailed below) - an object literal that sets the colors of the boxes and texts
	the object holds rank, name and score backing colors, font colors, and current colors for each
	whatever properties you provide will overwrite the default color - but you do not have to provide them all
	Note: that darker is just a ZIM shortcut to #111111, etc. you can use any CSS style color like "green", etc.
	{
		rankColor: light,
		rankBackgroundColor: darker,
		currentRankColor: white,
		currentRankBackgroundColor: pink,

		nameColor: darker,
		nameBackgroundColor: lighter,
		currentNameColor: darker,
		currentNameBackgroundColor: "#f0d2e8",

		scoreColor: darker,
		scoreBackgroundColor: light,
		currentScoreColor: darker,
		currentScoreBackgroundColor: "#f0d2e8"
	}
total - (default 10) the number of rows to show
scoreWidth - (default 300) the width of the score field (the whole grid will be scaled to fit inside the width and height)
scorePlaces - (default 6) the maximum number of digits expected
scoreZeros - (default false) set to true to fill in zeros up to the scorePlaces- ie. 00023
spacing - (default 10) the spacing between the fields (the whole grid will be scaled to fit inside the width and height)
arrows - (default false) set to true to show arrows above and below the name when the player enters their name
borderColor - (default null) the border color
borderWidth - (default null unless borderColor then default 2) the border thickness
shadowColor - (default rgba(0,0,0,.3)) the shadow color - set to -1 for no shadow
shadowBlur - (default 14) set to 0 for no shadow
reverse - (default false) put smallest score at top
allowZero - (default false) allow a zero score - good for when reverse is set
font - (default courier) the font for the store name and score
fontSize - (default 60) the font size
nameShift - (default 0) vertical shift for the name if the font is off center
scoreShift - (default 0) vertical shift for the score if the font is off center
rankShift - (default 0) vertical shift for the rank if the font is off center

METHODS
score(score) - set a score - will allow user to enter initials
save() - this method is automatically done by save button when initials are filled in
cancel() - this method is automatically called when the user presses off the board without saving
startTime() - start timing the game - if a points per minute was provided during ZIM LeaderBoard setup (not for localStorage or manual)
stopTime() - stop timing the game - if a points per minute was provided during ZIM LeaderBoard setup (not for localStorage or manual)
redraw(newData, newWinner) - this method is automatically done at various points to redraw the grid with new data
	newData is in an array with score objects like so: [{player:"RAJ", score:20}, {player:"DAN", score:18}]
	newWinner is an optional index of the winner - who will then be able to enter their id
	leave out the newWinner to show the board with current scores set (no blanks for entering name)
dispose() - remove event listeners - you need to remove the LeaderBoard from the stage

NOTE: if you are using manual data then you can use any of these methods manually
NOTE: the leaderBoard extends a zim.Container so also has all container methods

PROPERTIES
winner - the index of the current winner if there is one (name getting entered) otherwise null
place - the index of the player's score even if not on the LeaderBoard
backing - reference to the zim.Rectangle used for the backing
backdrop - reference to the zim.Rectangle used for the backdrop to press off the LeaderBoard
filled - true if the winner name is filled in with three characters else false
grid - reference to the zim.Container used for the grid
	the grid has row containers and each row has a rank stepper, three name steppers and a score Label
	access these with:
		var score2 = grid.getChildAt(1).getChildAt(4);
	or to loop through all:
		zim.loop(grid, function(row, i) {
			var rank = row.getChildAt(0);
			var name1 = row.getChildAt(1);
			var name2 = row.getChildAt(2);
			var name3 = row.getChildAt(3);
			var score = row.getChildAt(4);
		});
titleText - reference to the zim.Label used for the title if the title parameter is provided
dataSource - get the of data used as a string: database (using an ID), localStorage or manual (custom)
	NOTE: if there is a problem with your data ID then this will revert from database to localStorage
	You can tell when you save a score / name the save button says LOCAL if localStorage and SENT if database or manual
key - the ID (from http://zimjs.com/leaderboard/) if provided

NOTE: the leaderBoard extends a zim.Container so also has all container properties

EVENTS
dispatches a change event for each time the name letters are changed changed
dispatches a press event for when the save button is pressed
dispatches a save event for when the data is saved in localStorage or database (key) mode but not manual mode
dispatches a close event only when the board is closed by pressing off the board
dispatches an error event if data connection is not made properly
dispatches a cancel event when a new score is requested before an older score is saved
--*///+100

	// THE CODE FOR THE GAME MODULE IS LINKED TO AT THE TOP OF THE DOCS

	//-100

/*--
zim.Meter = function(stage, vertical, horizontal, color, textColor, padding, decimals, alpha, skew)

Meter
zim class - extends a zim.Label
** MUST import game module - SEE link at top of Docs for GAME under ZIM HELPER modules

DESCRIPTION
A meter that shows measured FPS in a text label

NOTE as of ZIM 5.5.0 the zim namespace is no longer required (unless zns is set to true before running zim)

EXAMPLE
var meter = new Meter(stage, "top", "left"); // adds to stage at top left - default bottom left
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below

stage - the stage
vertical (default "bottom") set to "top" for top - no middle
horizontal (default "left") set to "right" for right - no middle
color (default green) color of backing
textColor (default "black") color of text
padding (default 20) the distance from Meter to edge
decimals (default 0) the number of decimals to show
alpha (default .6) the alpha of the Meter
skew (default 10) the skewX of the Meter

METHODS
dispose()

PROPERTIES
text - the text of the meter
position(vertical, horizontal) - reset the position

Plus all the methods and properties of a Label
--*///+105

	// THE CODE FOR THE GAME MODULE IS LINKED TO AT THE TOP OF THE DOCS

	//-105

/*--
zim.Board = function(size, num, backgroundColor, rollBackgroundColor, borderColor, borderWidth, isometric, indicatorColor, indicatorBorderColor, indicatorBorderWidth, indicatorSize, indicatorType, arrows, arrowColor, arrowRollColor, info, labels, color, scaleMin, scaleMax, swipe)

Board
zim class - extends a zim.Container which extends a createjs.Container
** MUST import game module - SEE link at top of Docs for GAME under ZIM HELPER modules

DESCRIPTION
Creates a square tiled board with num columns and rows.
This can be either isometric (default) or top view.
The info for the board can be for any number of rows and columns
that is the same size or bigger than the num for the board.
The info does not need to have the same number of columns and rows
The info holds data, color, icon and items properties
and is an outer array of rows and inner arrays for columns.
The info array can be specified as a parameter
or adjusted as a property later with array manipulations
but board.update() must be called to view the changes.

Board offers many properties and methods
to add and remove items, set colors, icons and data per square,
move the board and move items along paths, etc.

There is a powerful filter parameter that uses subsets of the info
This can be used to find random tiles with certain info
or to allow key presses only to certain tiles.
The filter is positive and negative to include or not include the following:
data, notData, color, notColor, icon, notIcon, info, notInfo, col, notCol, row, notRow
in each case it can be a single value or an array of values
for instance board.getRandomTile({data:"x", notItem:[tree, bush, rock]}); etc.

See: https://zimjs.com/iso/ for a full example

Note: as of game 2.0, the zim namespace is not required unless zns is true

EXAMPLE
// for a more complete example see https://zimjs.com/iso/
var board = new Board().center();
var person = new Person();
board.add(person, 5, 5);
var tile = board.getTile(2,3);
board.setColor(tile,red);
board.setData(tile,"0");
board.addKeys(person,"arrows",{notData:"0"});
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
size - (default 50) the square tile size in pixels
num - (default 8) the number of visible squares on the board (cols, rows)
backgroundColor - (default "#eee") the square color of the board
	see the info parameter/property and setColor() to set individual colors
rollBackgroundColor - (default "#aaa") the rollover square color of the board
borderColor - (default "#111") the border color of the squares
borderWidth - (default 2) the border width of the squares
icon - (default null) the default DisplayObject to clone add to each tile
	set the icon.type property to be included in filters - eg. icon.type = "poisonivy"
	see the info parameter/property and setTile() to set individual tiles
isometric - (default true) set to false to see a top view
	isometric is rotating the board 45 and scaling the container by 2 in the width
indicatorColor - (default "black") the indicator shows a path with showPath()
indicatorBorderColor - (default "#111") the border color of the indicator
indicatorBorderWidth - (default 8) the width of the border
indicatorSize - (default 2)
indicatorType - (default "circle") set to "circle" or "square" for the whole tile
arrows - (default true) show (auto) arrows if info is larger than board view
	arrows will move the "camera" rather than move the board
	so pressing right will move the board to the left, etc.
arrowColor - (default "rgba(0,0,0,.4)") the arrow color
arrowRollColor - (default "white") the arrow roll color
info - (default null) set to an array of rows and cols in the following format:
	Rows are the outer array and cols are the inner arrays with {} being the info
	[[{data:"x", color:blue, items:[new Person()]},{},{},,,,,], [,,],,,,,,,,,,,,,,,]
		data (default "x"): any data desired - could be data for path finding or an array, etc.
		color (default backgroundColor): the color of the tile on the board when showing this info
		items (default []): a list of items to display on the tile when showing this info
	The info will be automatically filled in so just commas will do as placeholders
	The max length of the inner arrays will be used for all inner arrays
	The final length of rows and cols will be available as numCols and numRows
	See also the methods for ways to add data, color, icon and items
	Or use the board.info property any time to change the info - may need to board.update() after
	The info may be bigger than the board size and does not have to be square like the board
labels - (default false) set to true to display the data from info in tiles - for debugging
color - (default "#666") color of the labels
scaleMin - (default 1.2) the minimum items will be scaled when farther (top) from user
scaleMax - (default 1.8) the maximum items will be scaled when closer (bottom) to user
swipe - (default true) set to false to not swipe on the board to move the board (like arrows)
buffer - (default 3) nudge camera when moving item is this many tiles from the edge
	applies when info is larger than board and keys or path is used to move item
	set to 0 to not nudge camera

METHODS
**FILTER - some methods have an optional filter parameter
	this will limit the method to work on a subset of the info array or tiles
	the filter parameter is an object {} with any number of the following properties:
	Note: for convenience, these also work in plural - for instance colors, notItems, cols, etc.
		data - a value or array of data the info/tile must have
		notData - a value or array of data the info/tile must NOT have
		color - a value or array of color the info/tile must have
		notColor - a value or array of color the info/tile must NOT have
		icon - a value or array of the icon's type property the info/tile must have
		notIcon - a value or array of the icon's type property the info/tile must NOT have
		item - a value or array of items the info/tile must have
		notItem - a value or array of items the info/tile must NOT have
		col - a value or array of cols (in visible board) the info/tile must be in
		notCol - a value or array of cols (in visible board) the info/tile must NOT be in
		row - a value or array of rows (in visible board) the info/tile must be in
		notRow - a value or array of rows (in visible board) the info/tile must NOT be in
positionBoard(i, j) - move the board to show data from info at j row and i col
	see also startCol and startRow properties and moveRow() below
moveCamera(dir) - moves the camera "left", "right", "up" or "down"
	this moves the board the opposite direction by one tile
	used internally by arrows and swiping
addCol(index) - add a column of default data at the given index (optional - at end of info if not provided)
addRow(index) - add a row of default data at the given index (optional - at end of info if not provided)
update() - updates data, colors and pieces for currently visible board
	call this if the info is manually updated with array manipulation
getTile(col, row) - get the tile at a given col and row
	isometric has cols from top to right and rows from top to left
getRandomTile(filter**) - get a random tile
	or a random tile within a filtered set - see **FILTER at top of METHODS
getIndexes(tile) - get an object with col and row properties for tile
	isometric has 0,0 is at top of board with cols going to right and
getPoint(a,b) - get tile point (x, y) in items container
	a is either tile (leave b blank) or pass in i and j info indexes
getGlobalPoint(a,b) - get the tile point (x, y) on global stage
	a is either tile (leave b blank) or pass in i and j info indexes
getInfo(a,b) - get the info object for a tile {data:data, color:color, items:[items]}
	a is either tile (leave b blank) or pass in i and j info indexes
getData(a,b) - get the data for a tile
	a is either tile (leave b blank) or pass in i and j info indexes
getColor(a,b) - get the color for a tile
	a is either tile (leave b blank) or pass in i and j info indexes
getIcon(a,b) - get the icon for a tile
	a is either tile (leave b blank) or pass in i and j info indexes
getItems(a,b) - get the items array for a tile
	a is either tile (leave b blank) or pass in i and j info indexes
getAllItems(filter**) - get an array of all items
	or pass in a filtered to get a filtered set - see **FILTER at top of METHODS
setData(tile, value) - set the data for a tile
setColor(tile, color) - set the color of a tile
	for setting items on the board use add() method
	for setting items off the board or for setting the info
	use board.info[j][j] = {data:data, color:color, items:[item, item, etc.]}
clearInfo(filter**) - clear all the info or a filtered set - see **FILTER at top of METHODS
clearData(filter**) - clear all the info data or a filtered set - see **FILTER at top of METHODS
clearColors(filter**) - clear all the colors (to default color) or a filtered set - see **FILTER at top of METHODS
clearIcons(filter**) - clear all the icons (to default icon) or a filtered set - see **FILTER at top of METHODS
clearItems(filter**) - clear all the items or a filtered set - see **FILTER at top of METHODS
setArrows() - sets arrows to handle moving camera
	these will automatically be show when needed (info is bigger than cols or rows)
removeArrows() - remove arrows
setArrowHover() - update hover - ZIM will do this automatically
setDepth() - set the stacking order of an item in items - ZIM will do this automatically
add(obj, col, row, data, color, icon) - add a DisplayObject to the board
	this adds the object to the items container - and adds various properties (so do not addTo manually)
	data is additional data - for instance that could be used in path finding
	icon is any DisplayObject that will be centered in the Rectangle of the tile
	set icon.type to be included in the filters - for instance, icon.type = "stone", icon.type = "trap"
remove(obj) - removes an object from the board
position(obj, col, row) - position an item at the col and row on the board
	to position an object off the board remove the object and use board.info[j][i].items.push(obj);
	the board will add the object when it needs to be shown
move(obj, col, row, time) - animates or places object a certain number of rows or cols on the board
showPath(path, time) - shows path indicators for a path
	path has a format of [[x,y],[x,y]] or [{x:col, y:row}, {etc.}, {etc.}]
	This accommodates an EasyStar path - see https://zimjs.com/iso/
clearPath() - clears the path
followPath(obj, path, time, animation, buffer) - animates object along a provided path
	path has a format of [[x,y],[x,y]] or [{x:col, y:row}, {etc.}, {etc.}]
	time (default 600 ms) is how long to take from one step to the next
	animation (default 300 ms) is how long to animate to get to the next step
	buffer (default 0) how many tiles from the edge before the camera is nudged
		a buffer of 0 will not nudge the camera
		but if the camera is moved, the path could lead the item off the board
		which is fine - if that is what is desired
stopFollowPath() - stops animation along path
shiftPath(lastStartX, startX, lastStartY, startY, obj) - move the path
	lastStartX is board startX before shifting and startX is the new board startX - same for y
	As the camera or board moves, the path coordinates need to change too
	ZIM does this automatically when using arrows or swiping and a path is visible
addKeys(obj, type, filter**) - add keyboard control to item - type is "arrows" or "wasd"
	optionally pass in a filter object - see **FILTER at top of METHODS
	to limit the parts of the board the item can be keyed to
	for instance, {data:["x", "g"]} allows moving to tiles with info data of "x" or "g" but not anything else
	Note: followPath() can only follow data so to sync these use the data filter
	Note: to change the filter, use removeKeys() and then addKeys() with a different filter
removeKeys(type) - remove the keyboard controls for a certain type - "arrows" or "wasd"

NOTE: the Board extends a zim.Container so also has all container methods

PROPERTIES
type - a reference to the type of object as a string
tiles - a reference to the ZIM Tile object that makes the board
pieces - a reference to the ZIM Container that holds all items on the board
num - a read only number of rows and cols in the board
size - a read only width and height of the tile (before isometrics applied)
info - the info array of rows and cols - see the info parameter for more... info
	when making an update to info, the update() method will be needed to see a change in the board
	note: using the arrows or swiping will automatically update so items off screen do not need updating
data - an array of rows and cols that represents the info's data property of the current board squares
	this can be passed in to path finding such as EasyStar to find a path on the board
	note: this is info data only for the tiles, not all of the info data from the info array
numCols - the total number of columns in the info (not the board)
numRows - the total number of rows in the info (not the board)
startCol - the index of the info to place at the left of the board
	this is one way to move the board and is used by the arrows and swiping
	also see the positionBoard(col, row) method
startRow - the index of the info to place at the top of the board
buffer - get or set how many squares moving item is from edge before camera is nudged
arrows - Boolean to get or set the arrows - if true, arrows will be applied when needed
isometric - get or set the isometric (true) or flat (false) view of the board
	the board is scaled up 20% going from iso to flat and down 20% going from flat to iso
	this just helps it visually appear similar in size
currentTile - a reference to the selected tile
lastTile - a reference to the tile selected before the current selected tile
path - a reference to the visible path if there is one showing

PROPERTIES for Tile
boardCol - the visible column index of the tile
boardRow - the visible row index of the tile

PROPERTIES for item
boardCol - get the visible column index of the tile (negative if left of board)
boardRow - get the visible column index of the tile (negative if right of board)
moving - get whether the item is moving (also moving event on board)
boardTile - get the tile under the item
square = a string of "row-col" - so "0-0", "0-1", "0-2", etc. and "3-2" is third row, second column

NOTE: the Board extends a zim.Container so also has all container properties

EVENTS
dispatches a "change" event for each time a different tile is selected or rolled over
dispatches a "positioned" event when item is finally placed on a tile
dispatches a "moving" event when item is being animated from tile to tile
dispatches a "movingdone" event when item is finished being animated (500ms delay)
--*///+110

	// THE CODE FOR THE GAME MODULE IS LINKED TO AT THE TOP OF THE DOCS

	//-110

/*--
zim.Person = function(shirt, pants, head, outline, player, cache)

Person
zim class - extends a zim.Container which extends a CreateJS Container
** MUST import game module - SEE link at top of Docs for GAME under ZIM HELPER modules

DESCRIPTION
A very generic person in isometric view sized to work with ZIM Board

See: https://zimjs.com/iso/ for a full example

EXAMPLE
var board = new Board().center();
board.add(new Person(), 5, 4); // random person
// or specify:
board.add(new Person(green,brown,dark), 4, 5);
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
shirt - (default random ZIM color) the shirt color - any CSS color is fine "gold", "#C33", etc.
pants - (default random ZIM color) the pants color
head - (default random skin color) the head color
outline - (default "#333") the outline color for the head
player - (default false) set to true if main player - then type="Me" not "Player"
cache - (default false) probably only cache if lots of players

PROPERTIES
type - either "Person" or "Me" if player parameter is true
** If added to ZIM Board receives these properties:
boardCol - get the visible column index of the tile (negative if left of board)
boardRow - get the visible column index of the tile (negative if right of board)
moving - get whether the item is moving (also moving event on board)
boardTile - get the tile under the item
square = a string of "row-col" - so "0-0", "0-1", "0-2", etc. and "3-2" is third row, second column

Plus all the methods and properties of a ZIM Container and CreateJS Container
Note: if added to a ZIM Board, use the Board methods to manipulate
--*///+115

	// THE CODE FOR THE GAME MODULE IS LINKED TO AT THE TOP OF THE DOCS

	//-115

/*--
zim.Orb = function(radius, color, color2, accentColor, accentColor2, flat, alpha, time, delay)

Orb
zim class - extends a zim.Container which extends a CreateJS Container

DESCRIPTION
A very generic orb with two styles - flat and shaded
** MUST import game module - SEE link at top of Docs for GAME under ZIM HELPER modules

See: https://zimjs.com/iso/ for a full example

EXAMPLE
var board = new Board().center();
board.add(new Orb(), 2, 2); // flat
// or shaded with pulse:
board.add(new Orb({flat:false, color2:pink, accentColor2:white, delay:5000}), 4, 2);
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
radius (default 16 flat or 22 not) - the radius of the orb
color (default blue flat or purple not) - the color of the orb - any CSS color is fine too
color2 (default #111 flat or null (color) not) - core color if flat, pulse color if shaded
accentColor (default null flat or pink not) - border color if flat, highlight color if shaded
accentColor2 (default null) - border color of core if flat, highlight color of pulse if shaded
flat (default true) - set to false to see shaded orb
alpha (default .5) - the alpha to animate the color to if flat (keeps core) or the pulse to if shaded
time (default 1000) - the time to animate the pulse (shaded only)
delay (default 1000) - the delay until the animation of the pulse happens (shaded only)

PROPERTIES
type - "Orb"
** If added to ZIM Board receives these properties:
boardCol - get the visible column index of the tile (negative if left of board)
boardRow - get the visible column index of the tile (negative if right of board)
moving - get whether the item is moving (also moving event on board)
boardTile - get the tile under the item
square = a string of "row-col" - so "0-0", "0-1", "0-2", etc. and "3-2" is third row, second column

Plus all the methods and properties of a ZIM Container and CreateJS Container
Note: if added to a ZIM Board, use the Board methods to manipulate
--*///+120

	// THE CODE FOR THE GAME MODULE IS LINKED TO AT THE TOP OF THE DOCS

	//-120

/*--
zim.Tree = function()

Tree
zim class - extends a zim.Container which extends a CreateJS Container
** MUST import game module - SEE link at top of Docs for GAME under ZIM HELPER modules

DESCRIPTION
A very generic tree in isometric view sized to work with ZIM Board

See: https://zimjs.com/iso/ for a full example

EXAMPLE
var board = new Board().center();
board.add(new Tree(), 6, 6);
END EXAMPLE

PROPERTIES
type - "Tree"
** If added to ZIM Board receives these properties:
boardCol - get the visible column index of the tile (negative if left of board)
boardRow - get the visible column index of the tile (negative if right of board)
moving - get whether the item is moving (also moving event on board)
boardTile - get the tile under the item
square = a string of "row-col" - so "0-0", "0-1", "0-2", etc. and "3-2" is third row, second column

Plus all the methods and properties of a ZIM Container and CreateJS Container
Note: if added to a ZIM Board, use the Board methods to manipulate
--*///+125

	// THE CODE FOR THE GAME MODULE IS LINKED TO AT THE TOP OF THE DOCS

	//-125

/*--
zim.Timer = function(time, step, colon, down, isometric, startPaused, size, font, color, backgroundColor, borderColor, borderWidth, align, valign, fontOptions, width, height)

Timer
zim class - extends a zim.Label which extends a CreateJS Container
** MUST import game module - SEE link at top of Docs for GAME under ZIM HELPER modules

DESCRIPTION
A timer that can count up or down and be set to isometric

See: https://zimjs.com/iso/ for a full example

EXAMPLE
var timer = new Timer({
	time:60,
	borderColor:dark,
	isometric:"left" // isometric for left side of board
}).loc(100,100);
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
time - (default 60) time in seconds to start the timer
step - (default 1000) step in ms to show a change in timer
colon - (default false) add colon and seconds eg. 1:30 instead of 90 seconds
down - (default true) set to false to count up
isometric - (default null) set to "left" or "right" to position an isometric timer
	the registration point is on the top right corner for a "right" isometric timer
	this makes aligning the tops of a Timer and a Scorer easier
startPaused - (default false) set to true to start the timer paused - then use timer.start(num)
size - (default 36) the size of the font in pixels
font - (default arial) the font or list of fonts for the text
color - (default "black") color of font (any CSS color)
align - ((default "center") text registration point alignment also "left" and "right"
valign - (default "center") vertical registration point alignment alse "middle / center", "bottom"
fontOptions - (default null) css VALUES as a single string for font-style font-variant font-weight
	eg. "italic bold small-caps" or just "italic", etc.
borderColor - (default null) the color of the border
borderWidth - (default null) thickness of the border
width - (default 150) the width of the timer - can also leave defaults and scale timer
height - (default 60) the height of the timer

METHODS
start(time) - start the timer
pause(state) - state defaults to true to pause, set to false to unpause
stop() - stop the timer and set paused to null - does not change time property

PROPERTIES
type - "Timer"
time - get or set the time of the timer
totalTime - get the start time of the timer
paused - get or set the paused state of the timer with Boolean true or false
isometric - get or set the isometric - false, "left", "right"
intervalID - access to the intervalID of the timer - use clearInterval(timer.intervalID)

Plus all the methods and properties of a ZIM Label and CreateJS Container

EVENTS
Dispatches a "step" event for each step the timer takes
Dispatches a "complete" event when the timer counts down to 0
there is no complete event for counting up - use logic in step event function to handle that
--*///+130

	// THE CODE FOR THE GAME MODULE IS LINKED TO AT THE TOP OF THE DOCS

	//-130

/*--
zim.Scorer = function(score, isometric, size, font, color, backgroundColor, borderColor, borderWidth, align, valign, fontOptions, width, height)

Scorer
zim class - extends a zim.Label which extends a CreateJS Container
** MUST import game module - SEE link at top of Docs for GAME under ZIM HELPER modules

DESCRIPTION
A score label with backing set and isometric settings

See: https://zimjs.com/iso/ for a full example

EXAMPLE
var score = new Score({backroundColor:yellow, color:black, isometric:"right"}).loc(stageW-100,100);
END EXAMPLE

PARAMETERS supports DUO - parameters or single object with properties below
score - (default 0) set the start score
isometric - (default null) set to "left" or "right" to position an isometric scorer
	the registration point is on the top right corner for a "right" isometric scorer
	this makes aligning the tops of a Timer and a Scorer easier
size - (default 36) the size of the font in pixels
font - (default arial) the font or list of fonts for the text
color - (default "black") color of font (any CSS color)
align - ((default "center") text registration point alignment also "left" and "right"
valign - (default "center") vertical registration point alignment alse "middle / center", "bottom"
fontOptions - (default null) css VALUES as a single string for font-style font-variant font-weight
	eg. "italic bold small-caps" or just "italic", etc.
borderColor - (default null) the color of the border
borderWidth - (default null) thickness of the border
width - (default 150) the width of the scorer - can also leave defaults and scale scorer
height - (default 60) the height of the scorer

PROPERTIES
type - "Scorer"
score - get or set the score
isometric - get or set the isometric - false, "left", "right"

Plus all the methods and properties of a ZIM Label and CreateJS Container
--*///+135

	// THE CODE FOR THE GAME MODULE IS LINKED TO AT THE TOP OF THE DOCS

	//-135


	return zim;
} (zim || {});
if (!window.zns) zimplify();
