--- /dev/null
+// Copyright (C) 2007 Chris Double.\r
+// \r
+// Redistribution and use in source and binary forms, with or without\r
+// modification, are permitted provided that the following conditions are met:\r
+// \r
+// 1. Redistributions of source code must retain the above copyright notice,\r
+// this list of conditions and the following disclaimer.\r
+// \r
+// 2. Redistributions in binary form must reproduce the above copyright notice,\r
+// this list of conditions and the following disclaimer in the documentation\r
+// and/or other materials provided with the distribution.\r
+// \r
+// THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,\r
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\r
+// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\r
+// DEVELOPERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;\r
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\r
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR\r
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\r
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+//\r
+function identity(x) {\r
+ return x;\r
+}\r
+\r
+function foldl(f, initial, seq) {\r
+ for(var i=0; i< seq.length; ++i) \r
+ initial = f(initial, seq[i]);\r
+ return initial;\r
+}\r
+\r
+var memoize = true;\r
+\r
+function ParseState(input, index) {\r
+ this.input = input;\r
+ this.index = index || 0;\r
+ this.length = input.length - this.index;\r
+ this.cache = { };\r
+ return this;\r
+}\r
+\r
+ParseState.prototype.from = function(index) {\r
+ var r = new ParseState(this.input, this.index + index);\r
+ r.cache = this.cache;\r
+ r.length = this.length - index;\r
+ return r;\r
+}\r
+\r
+ParseState.prototype.substring = function(start, end) {\r
+ return this.input.substring(start + this.index, (end || this.length) + this.index);\r
+}\r
+\r
+ParseState.prototype.trimLeft = function() {\r
+ var s = this.substring(0);\r
+ var m = s.match(/^\s+/);\r
+ return m ? this.from(m[0].length) : this;\r
+}\r
+\r
+ParseState.prototype.at = function(index) {\r
+ return this.input.charAt(this.index + index);\r
+}\r
+\r
+ParseState.prototype.toString = function() {\r
+ return 'PS"' + this.substring(0) + '"'; \r
+}\r
+\r
+ParseState.prototype.getCached = function(pid) {\r
+ if(!memoize)\r
+ return false;\r
+\r
+ var p = this.cache[pid];\r
+ if(p) \r
+ return p[this.index];\r
+ else\r
+ return false;\r
+}\r
+\r
+ParseState.prototype.putCached = function(pid, cached) {\r
+ if(!memoize)\r
+ return false;\r
+\r
+ var p = this.cache[pid];\r
+ if(p)\r
+ p[this.index] = cached;\r
+ else {\r
+ p = this.cache[pid] = { };\r
+ p[this.index] = cached;\r
+ }\r
+}\r
+\r
+function ps(str) {\r
+ return new ParseState(str);\r
+}\r
+\r
+// 'r' is the remaining string to be parsed.\r
+// 'matched' is the portion of the string that\r
+// was successfully matched by the parser.\r
+// 'ast' is the AST returned by the successfull parse.\r
+function make_result(r, matched, ast) {\r
+ return { remaining: r, matched: matched, ast: ast };\r
+}\r
+\r
+var parser_id = 0;\r
+ \r
+// 'token' is a parser combinator that given a string, returns a parser\r
+// that parses that string value. The AST contains the string that was parsed.\r
+function token(s) {\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+\r
+ var r = state.length >= s.length && state.substring(0,s.length) == s;\r
+ if(r) \r
+ cached = { remaining: state.from(s.length), matched: s, ast: s };\r
+ else\r
+ cached = false;\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ };\r
+}\r
+\r
+// Like 'token' but for a single character. Returns a parser that given a string\r
+// containing a single character, parses that character value.\r
+function ch(c) {\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+ var r = state.length >= 1 && state.at(0) == c;\r
+ if(r) \r
+ cached = { remaining: state.from(1), matched: c, ast: c };\r
+ else\r
+ cached = false;\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ };\r
+}\r
+\r
+// 'range' is a parser combinator that returns a single character parser\r
+// (similar to 'ch'). It parses single characters that are in the inclusive\r
+// range of the 'lower' and 'upper' bounds ("a" to "z" for example).\r
+function range(lower, upper) {\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+ \r
+ if(state.length < 1) \r
+ cached = false;\r
+ else {\r
+ var ch = state.at(0);\r
+ if(ch >= lower && ch <= upper) \r
+ cached = { remaining: state.from(1), matched: ch, ast: ch };\r
+ else\r
+ cached = false;\r
+ }\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ };\r
+}\r
+\r
+// Helper function to convert string literals to token parsers\r
+// and perform other implicit parser conversions.\r
+function toParser(p) {\r
+ return (typeof(p) == "string") ? token(p) : p;\r
+}\r
+\r
+// Parser combinator that returns a parser that\r
+// skips whitespace before applying parser.\r
+function whitespace(p) {\r
+ var p = toParser(p);\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+\r
+ cached = p(state.trimLeft());\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ };\r
+}\r
+\r
+// Parser combinator that passes the AST generated from the parser 'p' \r
+// to the function 'f'. The result of 'f' is used as the AST in the result.\r
+function action(p, f) {\r
+ var p = toParser(p);\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached) \r
+ return cached;\r
+\r
+ var x = p(state);\r
+ if(x) {\r
+ x.ast = f(x.ast);\r
+ cached = x;\r
+ }\r
+ else {\r
+ cached = false;\r
+ }\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ };\r
+}\r
+\r
+// Given a parser that produces an array as an ast, returns a\r
+// parser that produces an ast with the array joined by a separator.\r
+function join_action(p, sep) {\r
+ return action(p, function(ast) { return ast.join(sep); });\r
+}\r
+\r
+// Given an ast of the form [ Expression, [ a, b, ...] ], convert to\r
+// [ [ [ Expression [ a ] ] b ] ... ]\r
+// This is used for handling left recursive entries in the grammar. e.g.\r
+// MemberExpression:\r
+// PrimaryExpression\r
+// FunctionExpression\r
+// MemberExpression [ Expression ]\r
+// MemberExpression . Identifier\r
+// new MemberExpression Arguments \r
+function left_factor(ast) {\r
+ return foldl(function(v, action) { \r
+ return [ v, action ]; \r
+ }, \r
+ ast[0], \r
+ ast[1]);\r
+}\r
+\r
+// Return a parser that left factors the ast result of the original\r
+// parser.\r
+function left_factor_action(p) {\r
+ return action(p, left_factor);\r
+}\r
+\r
+// 'negate' will negate a single character parser. So given 'ch("a")' it will successfully\r
+// parse any character except for 'a'. Or 'negate(range("a", "z"))' will successfully parse\r
+// anything except the lowercase characters a-z.\r
+function negate(p) {\r
+ var p = toParser(p);\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+\r
+ if(state.length >= 1) {\r
+ var r = p(state);\r
+ if(!r) \r
+ cached = make_result(state.from(1), state.at(0), state.at(0));\r
+ else\r
+ cached = false;\r
+ }\r
+ else {\r
+ cached = false;\r
+ }\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ };\r
+}\r
+\r
+// 'end_p' is a parser that is successful if the input string is empty (ie. end of parse).\r
+function end_p(state) {\r
+ if(state.length == 0) \r
+ return make_result(state, undefined, undefined);\r
+ else\r
+ return false;\r
+}\r
+\r
+// 'nothing_p' is a parser that always fails.\r
+function nothing_p(state) {\r
+ return false;\r
+}\r
+\r
+// 'sequence' is a parser combinator that processes a number of parsers in sequence.\r
+// It can take any number of arguments, each one being a parser. The parser that 'sequence'\r
+// returns succeeds if all the parsers in the sequence succeeds. It fails if any of them fail.\r
+function sequence() {\r
+ var parsers = [];\r
+ for(var i = 0; i < arguments.length; ++i) \r
+ parsers.push(toParser(arguments[i])); \r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached) {\r
+ return cached;\r
+ }\r
+\r
+ var ast = [];\r
+ var matched = "";\r
+ var i;\r
+ for(i=0; i< parsers.length; ++i) {\r
+ var parser = parsers[i]; \r
+ var result = parser(state);\r
+ if(result) {\r
+ state = result.remaining;\r
+ if(result.ast != undefined) {\r
+ ast.push(result.ast);\r
+ matched = matched + result.matched;\r
+ }\r
+ }\r
+ else {\r
+ break;\r
+ }\r
+ }\r
+ if(i == parsers.length) {\r
+ cached = make_result(state, matched, ast);\r
+ }\r
+ else \r
+ cached = false;\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ };\r
+}\r
+\r
+// Like sequence, but ignores whitespace between individual parsers.\r
+function wsequence() {\r
+ var parsers = [];\r
+ for(var i=0; i < arguments.length; ++i) {\r
+ parsers.push(whitespace(toParser(arguments[i])));\r
+ }\r
+ return sequence.apply(null, parsers); \r
+}\r
+\r
+// 'choice' is a parser combinator that provides a choice between other parsers.\r
+// It takes any number of parsers as arguments and returns a parser that will try\r
+// each of the given parsers in order. The first one that succeeds results in a \r
+// successfull parse. It fails if all parsers fail.\r
+function choice() {\r
+ var parsers = [];\r
+ for(var i = 0; i < arguments.length; ++i) \r
+ parsers.push(toParser(arguments[i])); \r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached) {\r
+ return cached;\r
+ }\r
+ var i;\r
+ for(i=0; i< parsers.length; ++i) {\r
+ var parser=parsers[i];\r
+ var result = parser(state);\r
+ if(result) {\r
+ break;\r
+ }\r
+ } \r
+ if(i == parsers.length)\r
+ cached = false;\r
+ else\r
+ cached = result;\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
+// 'butnot' is a parser combinator that takes two parsers, 'p1' and 'p2'. \r
+// It returns a parser that succeeds if 'p1' matches and 'p2' does not, or\r
+// 'p1' matches and the matched text is longer that p2's.\r
+// Useful for things like: butnot(IdentifierName, ReservedWord)\r
+function butnot(p1,p2) {\r
+ var p1 = toParser(p1);\r
+ var p2 = toParser(p2);\r
+ var pid = parser_id++;\r
+ \r
+ // match a but not b. if both match and b's matched text is shorter\r
+ // than a's, a failed match is made\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+ \r
+ var br = p2(state);\r
+ if(!br) {\r
+ cached = p1(state);\r
+ } else {\r
+ var ar = p1(state);\r
+ if(ar.matched.length > br.matched.length)\r
+ cached = ar;\r
+ else\r
+ cached = false;\r
+ }\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
+// 'difference' is a parser combinator that takes two parsers, 'p1' and 'p2'. \r
+// It returns a parser that succeeds if 'p1' matches and 'p2' does not. If\r
+// both match then if p2's matched text is shorter than p1's it is successfull.\r
+function difference(p1,p2) {\r
+ var p1 = toParser(p1);\r
+ var p2 = toParser(p2);\r
+ var pid = parser_id++;\r
+ \r
+ // match a but not b. if both match and b's matched text is shorter\r
+ // than a's, a successfull match is made\r
+ return function(state) {\r
+ var savedState = sate;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+\r
+ var br = p2(state);\r
+ if(!br) {\r
+ cached = p1(state);\r
+ } else {\r
+ var ar = p1(state);\r
+ if(ar.matched.length >= br.matched.length)\r
+ cached = br;\r
+ else\r
+ cached = ar;\r
+ }\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
+\r
+// 'xor' is a parser combinator that takes two parsers, 'p1' and 'p2'. \r
+// It returns a parser that succeeds if 'p1' or 'p2' match but fails if\r
+// they both match.\r
+function xor(p1, p2) {\r
+ var p1 = toParser(p1);\r
+ var p2 = toParser(p2);\r
+ var pid = parser_id++;\r
+\r
+ // match a or b but not both\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+\r
+ var ar = p1(state);\r
+ var br = p2(state);\r
+ if(ar && br)\r
+ cached = false;\r
+ else\r
+ cached = ar || br;\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
+// A parser combinator that takes one parser. It returns a parser that\r
+// looks for zero or more matches of the original parser.\r
+function repeat0(p) {\r
+ var p = toParser(p);\r
+ var pid = parser_id++;\r
+ \r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached) {\r
+ return cached;\r
+ }\r
+\r
+ var ast = [];\r
+ var matched = "";\r
+ var result;\r
+ while(result = p(state)) {\r
+ ast.push(result.ast);\r
+ matched = matched + result.matched;\r
+ if(result.remaining.index == state.index)\r
+ break;\r
+ state = result.remaining; \r
+ } \r
+ cached = make_result(state, matched, ast); \r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
+// A parser combinator that takes one parser. It returns a parser that\r
+// looks for one or more matches of the original parser.\r
+function repeat1(p) {\r
+ var p = toParser(p);\r
+ var pid = parser_id++;\r
+\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+\r
+ var ast = [];\r
+ var matched = "";\r
+ var result= p(state);\r
+ if(!result) \r
+ cached = false;\r
+ else { \r
+ while(result) {\r
+ ast.push(result.ast);\r
+ matched = matched + result.matched;\r
+ if(result.remaining.index == state.index)\r
+ break;\r
+ state = result.remaining; \r
+ result = p(state);\r
+ } \r
+ cached = make_result(state, matched, ast); \r
+ }\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
+// A parser combinator that takes one parser. It returns a parser that\r
+// matches zero or one matches of the original parser.\r
+function optional(p) {\r
+ var p = toParser(p);\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached;\r
+ var r = p(state);\r
+ cached = r || make_result(state, "", false); \r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
+// A parser combinator that ensures that the given parser succeeds but\r
+// ignores its result. This can be useful for parsing literals that you\r
+// don't want to appear in the ast. eg:\r
+// sequence(expect("("), Number, expect(")")) => ast: Number\r
+function expect(p) {\r
+ return action(p, function(ast) { return undefined; });\r
+}\r
+\r
+function chain(p, s, f) {\r
+ var p = toParser(p);\r
+\r
+ return action(sequence(p, repeat0(action(sequence(s, p), f))),\r
+ function(ast) { return [ast[0]].concat(ast[1]); });\r
+}\r
+\r
+// A parser combinator to do left chaining and evaluation. Like 'chain', it expects a parser\r
+// for an item and for a seperator. The seperator parser's AST result should be a function\r
+// of the form: function(lhs,rhs) { return x; }\r
+// Where 'x' is the result of applying some operation to the lhs and rhs AST's from the item\r
+// parser.\r
+function chainl(p, s) {\r
+ var p = toParser(p);\r
+ return action(sequence(p, repeat0(sequence(s, p))),\r
+ function(ast) {\r
+ return foldl(function(v, action) { return action[0](v, action[1]); }, ast[0], ast[1]);\r
+ });\r
+}\r
+\r
+// A parser combinator that returns a parser that matches lists of things. The parser to \r
+// match the list item and the parser to match the seperator need to \r
+// be provided. The AST is the array of matched items.\r
+function list(p, s) {\r
+ return chain(p, s, function(ast) { return ast[1]; });\r
+}\r
+\r
+// Like list, but ignores whitespace between individual parsers.\r
+function wlist() {\r
+ var parsers = [];\r
+ for(var i=0; i < arguments.length; ++i) {\r
+ parsers.push(whitespace(arguments[i]));\r
+ }\r
+ return list.apply(null, parsers); \r
+}\r
+\r
+// A parser that always returns a zero length match\r
+function epsilon_p(state) {\r
+ return make_result(state, "", undefined);\r
+}\r
+\r
+// Allows attaching of a function anywhere in the grammer. If the function returns\r
+// true then parse succeeds otherwise it fails. Can be used for testing if a symbol\r
+// is in the symbol table, etc.\r
+function semantic(f) {\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached; \r
+ cached = f() ? make_result(state, "", undefined) : false;\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
+// The `and` predicate asserts that a certain conditional\r
+// syntax is satisfied before evaluating another production. Eg:\r
+// sequence(and("0"), oct_p)\r
+// (if a leading zero, then parse octal)\r
+// It succeeds if 'p' succeeds and fails if 'p' fails. It never \r
+// consume any input however, and doesn't put anything in the resulting\r
+// AST.\r
+function and(p) {\r
+ var p = toParser(p);\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached; \r
+ var r = p(state);\r
+ cached = r ? make_result(state, "", undefined) : false;\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+ \r
+// The opposite of 'and'. It fails if 'p' succeeds and succeeds if\r
+// 'p' fails. It never consumes any input. This combined with 'and' can\r
+// be used for 'lookahead' and disambiguation of cases.\r
+//\r
+// Compare:\r
+// sequence("a",choice("+","++"),"b")\r
+// parses a+b\r
+// but not a++b because the + matches the first part and peg's don't\r
+// backtrack to other choice options if they succeed but later things fail.\r
+//\r
+// sequence("a",choice(sequence("+", not("+")),"++"),"b")\r
+// parses a+b\r
+// parses a++b\r
+//\r
+function not(p) {\r
+ var p = toParser(p);\r
+ var pid = parser_id++;\r
+ return function(state) {\r
+ var savedState = state;\r
+ var cached = savedState.getCached(pid);\r
+ if(cached)\r
+ return cached; \r
+ cached = p(state) ? false : make_result(state, "", undefined);\r
+ savedState.putCached(pid, cached);\r
+ return cached;\r
+ }\r
+}\r
+\r
var k = classStatics[i];
NewClass[k] = ClassFactory[k];
}
+ NewClass.instantiate = instantiate.partial(NewClass);
// Copy parent methods, then add new instance methods
for (var k in parentMembers)
},
emit : function emit(evtname, trigger, data){
- var q = this.queues[evtname]
+ var evt, A = arguments
+ , q = this.queues[evtname]
, L = q ? q.length : 0
;
- if ( !L )
- return this;
-
- q = q._o.slice(0);
- var A = arguments
- , target = this.target
- , evt = new Event(evtname, target, trigger, data)
- , method = (A.length > 3 ? 'apply' : 'call')
- , args = (A.length > 3 ? [evt].concat(Y(A,3)) : evt)
- ;
- for (var i=0, fn=q[i]; i<L; fn=q[++i])
- fn[method](target, args);
+ if ( L ) {
+ q = q._o.slice(0);
+ var target = this.target
+ , evt = new Event(evtname, target, trigger, data)
+ , method = (A.length > 3 ? 'apply' : 'call')
+ , args = (A.length > 3 ? [evt].concat(Y(A,3)) : evt)
+ ;
+ for (var i=0, fn=q[i]; i<L; fn=q[++i])
+ fn[method](target, args);
+ }
- if (this.parent && !evt._stopped)
+ if (this.parent && !(evt && evt._stopped) )
this.parent.emit.apply(this.parent, A);
return evt;
},
/**
- * Dispatches the given event to all listeners in the appropriate queue.
- * Listeners are invoked with the event, and with context set to the target.
- * XXX: does not handle degenerate or recursive event dispatch
- */
- dispatchEvent : function dispatchEvent(evt){
- var queue = this.queues[evt.type];
- if (queue && queue.length)
- queue.invoke('call', evt.target, evt);
- if (this.parent)
- this.parent.emit(evt.type, evt.trigger, evt.data);
- return evt;
- },
-
- /**
* Decorates object with bound methods to act as a delegate of this hub.
*/
decorate : function decorate(delegate){
return delegate;
}
}
+
+
+, GLOBAL_ID = 0
,
Emitter =
Y.YObject.subclass('Emitter',
Y.extend({
'init' : function(target, parent){
+ if (this.__id__ === undefined)
+ this.__id__ = GLOBAL_ID++;
+
this.queues = {};
this.target = target || this;
Emitter.methods = methods;
// Global Event Hub
-Emitter.global = new Emitter()
+Emitter.global = new Emitter();
Emitter.global.decorate(exports);
Y['event'] = exports;
--- /dev/null
+var Y = require('Y').Y
+, event = require('Y/modules/y.event')
+, Event = event.Event
+, Emitter = event.Emitter
+,
+
+/**
+ * A simple event.
+ * TODO: Detect jQuery event objects.
+ * TODO: If DOM event, wrap with consistent API (use jQuery if present).
+ */
+PolyEvent =
+exports['PolyEvent'] =
+Event.subclass('PolyEvent', {
+ _stopped : false, // whether propagation is stopped
+
+ type : 'event', // event type
+ target : null, // target of the emitter
+ trigger : null, // object which caused this event
+ data : null, // event data
+
+
+
+ init : function init( type, target, trigger, data ){
+ data = data || {};
+ for (var k in data) this[k] = data[k];
+ this.data = this._o = data;
+
+ this.type = type;
+ this.target = target || trigger;
+ this.trigger = trigger || target;
+ },
+
+ has : function has(){
+
+ },
+
+ hasNoun : function hasNoun(noun){
+
+ },
+
+ hasVerb : function hasVerb(verb){
+
+ },
+
+ toString: function(){ return "PolyEvent("+this.type+")"; }
+})
+
+/**
+ * A simple multicaster.
+ */
+, methods = {
+
+ emit : function emit(evtname, trigger, data){
+ var q = this.queues[evtname]
+ , L = q ? q.length : 0
+ ;
+ if ( !L )
+ return this;
+
+ q = q._o.slice(0);
+ var A = arguments
+ , target = this.target
+ , evt = new PolyEvent(evtname, target, trigger, data)
+ , method = (A.length > 3 ? 'apply' : 'call')
+ , args = (A.length > 3 ? [evt].concat(Y(A,3)) : evt)
+ ;
+ for (var i=0, fn=q[i]; i<L; fn=q[++i])
+ fn[method](target, args);
+
+ if (this.parent && !evt._stopped)
+ this.parent.emit.apply(this.parent, A);
+
+ return evt;
+ },
+
+ /**
+ * @param {String} evt Event string to normalize and expand.
+ * @return {PolyEvent...}
+ */
+ getMatchingEvents : function getMatchingEvents(evt){
+
+ },
+
+ /**
+ * Decorates object with bound methods to act as a delegate of this hub.
+ */
+ decorate : function decorate(delegate){
+ if (!delegate)
+ return delegate;
+
+ Emitter.fn.decorate.call(this, delegate);
+ for (var k in methods)
+ delegate[k] = methods[k].bind(this);
+ return delegate;
+ }
+}
+,
+
+PolyEmitter =
+exports['PolyEmitter'] =
+Emitter.subclass('PolyEmitter',
+ Y.extend({
+ 'init' : function(target, parent){
+ Emitter.init.call(this, target, parent);
+ }
+ }, methods) )
+;
+PolyEmitter.methods = methods;
+
+// Global Event Hub
+PolyEmitter.global = new PolyEmitter();
+PolyEmitter.global.decorate(exports);
+
+
+// Helper Functions
+
+var _match_cache = {};
+
+/**
+ * Returns an array of all events which match the given event.
+ * @param event
+ */
+function getMatchingEvents(event, base_event) {
+ if ( !_match_cache[event] ) {
+
+ // Segregate event groups in a manner worthy of strict scrutiny
+ var seg = event.split('.').reduce(
+ function(seg, token){
+ if (!base_event)
+ base_event = token;
+ if (token.indexOf('-') !== -1)
+ seg.special.push(token);
+ else
+ seg.plain.push(token);
+ return seg;
+ }, { plain:[], special:[] });
+
+ // Calculate the correct powerset for each event-group
+ var evtset;
+ if ( !seg.special.length )
+ evtset = powerset( seg.plain ).map(sorter).map(joiner);
+ else {
+ evtset = propertyset( seg.special ).reduce(
+ function(acc, t) {
+ return acc.concat(
+ powerset( seg.plain.concat([ t ]) ).map(sorter).map(joiner) );
+ }, new Y.YArray());
+ }
+ // Remove invalid permutations and duplicates
+ _match_cache[event] = evtset
+ .filter(function(v) {
+ return v.indexOf(base_event) === 0;
+ })
+ .unique()
+ .reverse();
+
+ // Always notify 'all'
+ _match_cache[event].push('all');
+ }
+
+ // console.log("getMatchingEvents( event="+event+", base_event="+base_event+" ) --> [ "+_match_cache[event].join(",\n ")+" ]");
+ return _match_cache[event];
+}
+
+/**
+ * Calculates the set of all unique subsets of S (without regard for order) of size k (ie, combination (S k).
+ * @param {Array} S A collection.
+ * @param {Number} k Length of combinations to calculate.
+ * @return {Array} Array of sub-Arrays of S.
+ */
+function comb(S, k){
+ if ( !S || k <= 0 )
+ return new Y.YArray();
+
+ if (k === 1)
+ return Y(S).map(mkArray);
+
+ return S.reduce(function nComb(acc, v, i){
+ var subsets = comb(S.slice(i+1), k-1);
+ return acc.concat( subsets.invoke('concat', [v]) );
+ }, new Y.YArray());
+}
+function mkArray(v){ return new Y.YArray([v]); }
+
+/**
+ * Calculates the powerset of S (set of all subsets, P(S)).
+ * @param {Array} S A collection.
+ * @param {Number} [n] Optional upper-bound on subset size. Defaults to the length of S.
+ * @return {Array} Array of sub-Arrays of S.
+ */
+function powerset(S, n){
+ n = (n < 0 || n === undefined) ? S.length : (!S ? 0 : n);
+ for (var P = new Y.YArray(), i=1; i <= n; ++i)
+ P.extend( comb(S,i) );
+ return P;
+}
+
+/**
+ * @param {Array} T Array of Event-part Arrays.
+ * @return {Array}
+ */
+function propertyset(T){
+ var ps = new Y.YArray();
+ if ( !T || !T.length )
+ return ps;
+
+ return T.reduce(function(acc, t){
+ return acc.extend(t.split('-'), t);
+ }, ps)
+ .concat(T, powerset(T).map(joiner))
+ .unique();
+}
+
+var _prefixes = [ 'state', 'core', 'chrome', 'video', 'share' ]
+, _suffixes = [ 'user', 'auto' ]
+;
+function cmp( a, b ) {
+ var a_suffix = _suffixes.indexOf(a) !== -1
+ , b_suffix = _suffixes.indexOf(b) !== -1
+ , a_prefix = _prefixes.indexOf(a) !== -1
+ , b_prefix = _prefixes.indexOf(b) !== -1
+ ;
+ if (a_prefix === b_prefix && a_suffix === b_suffix)
+ return 0;
+ else if (a_prefix || b_suffix)
+ return -1;
+ else
+ return 1;
+}
+
+function shove(A, v){ A.push(v); return A; }
+function sorter(A){ A.sort(cmp); return A; }
+function joiner(A){ return A.join('.'); }
+
+function unique(A){
+ return A.reduce(function(acc, v) {
+ return (acc.indexOf(v) === -1) ? shove(acc, v) : acc;
+ }, Y([]));
+}
+
+
+Y.extend( exports, { comb:comb, powerset:powerset, propertyset:propertyset, unique:unique, getMatchingEvents:getMatchingEvents });
+Y.extend( Y['event'], exports );
// Add YArray to the things that count as arrays
type.isArray.types.push(YArray);
- var newYArray = YArray.instantiate.bind(YArray);
+ var newYArray = YArray.instantiate;
+ // var newYArray = YArray.instantiate.bind(YArray);
proxy({ target:YArray, donor:_Array, context:'_o',
names:'indexOf lastIndexOf shift join'.split(' ') });
proxy({ target:YArray, donor:_Array, context:'_o', wrap:newYArray,
names:['slice'] });
+ function unwrapY(o){
+ return (o instanceof YArray ? o.end() : o);
+ }
+
this['init'] =
function initYArray(o){
- if (o instanceof YArray)
- this._o = o._o;
- else
- this._o = o || [];
+ this._o = (
+ ( o === undefined ? [] :
+ ( o instanceof Array ? o :
+ ( o instanceof YArray ? o._o :
+ [o] )))); // lisp strikes back!
};
this['get'] =
return v;
};
- /**
- * Intersects this YArray with another collection, returning a new YArray.
- * The membership test uses Y(a).has(), so it is possible to intersect collections of different types.
- * For YArray and YObject, .has() uses strict equality (===) via .indexOf().
- *
- * @param {Array|Object|YCollection} a Comparison collection.
- * @return {YArray} A new YArray of all elements in {this} found in the supplied collection.
- *
- * var foo = /foo/;
- * var A = [foo, 'A', 1, 2, 3, 'C', /foo/];
- * var B = [foo, 'B', 3, 'A', 1, /foo/];
- * var I = Y(A).intersect(B);
- * I.toString() === "YArray([/foo/,A,1,3])"; // true
- * I.get(0) === foo; // true
- */
- this['intersect'] =
- function intersect(a){
- var A = Y(a);
- return this.filter(A.has, A);
- };
-
this['clear'] =
function clear(){
var A = this._o;
return this;
};
- function unwrapY(o){
- return (o instanceof YArray ? o.end() : o);
- }
-
this['concat'] =
function concat( donor ){
var A = this._o;
}, Y({}), this );
};
+ /**
+ * Set Intersection (A ^ B)
+ * Intersects this YArray with another collection, returning a new YArray.
+ * The membership test uses Y(a).has(), so it is possible to intersect collections of different types.
+ * For YArray and YObject, .has() uses strict equality (===) via .indexOf().
+ *
+ * @param {Array|Object|YCollection} a Comparison collection.
+ * @return {YArray} A new YArray of all elements in {this} found in the supplied collection.
+ *
+ * var foo = /foo/;
+ * var A = [foo, 'A', 1, 2, 3, 'C', /foo/];
+ * var B = [foo, 'B', 3, 'A', 1, /foo/];
+ * var I = Y(A).intersect(B);
+ * I.toString() === "YArray([/foo/,A,1,3])"; // true
+ * I.get(0) === foo; // true
+ */
+ this['intersect'] =
+ function intersect(a){
+ var A = Y(a);
+ return this.filter(A.has, A);
+ };
+
+ /**
+ * Set Union (A v B)
+ * @param {Array|Object|YCollection} a Comparison collection.
+ * @return {YArray} A new YArray of all elements in both collections, but without duplicates.
+ */
+ this['union'] =
+ function union(a){
+ return this.concat(a).unique();
+ };
+
+ /**
+ * Set Difference (A - B)
+ * @param {Array|Object|YCollection} a Comparison collection.
+ * @return {YArray} A new YArray of only elements in this not in supplied collection.
+ */
+ this['difference'] =
+ function difference(a){
+ var A = Y(a);
+ return this.filter(Y(A.has).compose(op.not), A);
+ };
+
+ // Symmetric Difference
+ this['xor'] =
+ function xor(a){
+ return this.difference(a).concat( Y(a).difference(this) );
+ };
+
+
return this;
});
-function _composer(x,fn){ return fn.call(this, x); }
+
function compose(f,g){
var fns = slice.call(arguments).map(_Function.toFunction);
- return function(){
- return fns.reduce(_composer, slice.call(arguments), this);
+ return function(x){
+ var self = this;
+ return fns.reduce(function (x, fn){
+ return fn.call(self, x);
+ }, x);
};
}
exports['YString'] =
YCollection.subclass('YString', function(YString){
- var newYString = YString.instantiate.bind(YString);
+ // var newYString = YString.instantiate.bind(YString);
+ var newYString = YString.instantiate;
proxy({ target:YString, donor:String, context:'_o', wrap:newYString,
names:'slice substr substring concat replace toLowerCase toUpperCase'.split(' ') });
proxy({ target:YString, donor:String, context:'_o',
, Emitter = require('Y/modules/y.event').Emitter
, unwrap = require('Y/types/function').unwrap
-, isFunction = Y.isFunction
+// , isFunction = Y.isFunction
, KNOWN_CLASSES = Class.KNOWN_CLASSES = exports['KNOWN_CLASSES'] = {}
, P = 'prototype'
}, instance);
var initialise = instance.__initialise__;
- if ( isFunction(initialise) )
+ if ( typeof initialise == 'function' )
initialise.apply(instance, args);
cls.emit('created', instance, {
Y.bindAll(instance, binds);
var init = cls.fn.init;
- if ( isFunction(init) ) {
+ if ( typeof init == 'function' ) {
var result = init.apply(instance, arguments);
if (result) instance = result; // XXX: I think this needs to go away
}
, parentMembers = {}
;
- if ( !members && !isFunction(Parent) ) {
+ if ( !members && !(typeof Parent == 'function') ) {
members = Parent;
Parent = null;
}
Parent = Object;
// Parent is the prototype
- if ( !isFunction(Parent) ) {
+ if ( !(typeof Parent == 'function') ) {
SuperClass = getProto(Parent).constructor || Object;
prototype = Parent;
}
// Either invoke body constructor...
- if ( isFunction(members) ) {
+ if ( typeof members == 'function' ) {
// body fn is responsible for calling mixin, attaching statics, etc
members.call(prototype, NewClass);
// Notify mixins to let them finish up any customization
if (mixins.length)
mixins.forEach(function(mxn){
- if ( mxn && isFunction(mxn.emit) )
+ if ( mxn && (typeof mxn.emit == 'function') )
mxn.emit('mixin', NewClass, { 'mixin':mxn, 'cls':NewClass });
});
},
/**
+ * @param {String} key
* @return The merged pairs at key taken from all bases.
*/
- aggregate : function aggregate(key){
- return this.__bases__
- .clone()
- .unshift(this)
- .reverse()
- .reduce(function(acc, base){
- var proto = (base instanceof Function ? base.prototype : base);
- return Y.extend(acc, proto[key]);
- }, {});
- }
+ aggregate : Y(aggregate).methodize()
}
return;
// Register onCreate to fire whenever a new instance is initialized
- if ( isFunction(cls.on) && hasOwn.call(mproto,'onCreate') && isFunction(onCreate) )
+ if ( (typeof cls.on == 'function') && hasOwn.call(mproto,'onCreate') && (typeof onCreate == 'function') )
cls.on('create', onCreate);
// Fire the mixin event on this Mixin only if we're done constructing the class
- if ( isFunction(mxn.emit) && mixin.caller !== Class )
+ if ( (typeof mxn.emit == 'function') && mixin.caller !== Class )
mxn.emit('mixin', cls, { 'mixin':mxn, 'cls':cls });
});
return cls;
}
+function aggregate(cls, key){
+ if ( !(cls instanceof Class) && cls.__class__ )
+ cls = cls.__class__;
+ return cls.__bases__
+ .clone()
+ .unshift(cls)
+ .reverse()
+ .reduce(function(acc, base){
+ var proto = (base instanceof Function ? base.prototype : base);
+ return Y.extend(acc, proto[key]);
+ }, {});
+}
+
+
Mixin.on('subclass',
function onMixinSubclass(evt){
var d = evt.data
, onMixin = members.onMixin
;
- if ( isFunction(onMixin) )
+ if ( typeof onMixin == 'function' )
mxn.on('mixin', onMixin);
// console.log('Mixin.subclass()', mxn, '<', d.parent, 'onMixin:', onMixin);
});
+
// Expose
exports['Class'] =
exports['subclass'] = Class;
exports['fabricate'] = Y.fabricate;
exports['lookupClass'] = lookupClass;
exports['mixin'] = mixin;
+exports['aggregate'] = aggregate;
var Y = require('Y').Y
+, layerable = require('ezl/layer/layerable')
, layer = require('ezl/layer/layer')
, html = require('ezl/layer/html')
;
Y.core.extend(exports, {
+ 'layerable' : layerable,
+ 'Layerable' : layerable.Layerable,
+
'layer' : layer,
'Layer' : layer.Layer,
-//#ensure "jquery"
-var Y = require('Y').Y
-, op = require('Y/op')
-
-, evt = require('evt')
-
-, Vec = require('ezl/math/vec').Vec
-, Loc = require('ezl/loc/loc').Loc
-, BoundingBox = require('ezl/loc/boundingbox').BoundingBox
-, Animation = require('ezl/loop/fx').Animation
-,
-
-CONTEXT_ATTRS = Y('globalAlpha globalCompositeOperation strokeStyle fillStyle lineWidth lineCap lineJoin miterLimit shadowOffsetX shadowOffsetY shadowBlur shadowColor'.split(' ')),
-FAUX_ACCESSORS = Y('width height position stroke fill origin rotate scale translate title'.split(' '))
-, _X = 0, _Y = 1
+var Y = require('Y')
+, evt = require('evt')
+, Layerable = require('ezl/layer/layerable').Layerable
,
-
-
Layer =
exports['Layer'] =
-new evt.Class('Layer', {
- __bind__ : [ 'tick' ],
-
- /// Class Defaults ///
-
- // _layer* is applied to this.layer (the DOM Element)
- _layerHtml : '<div/>', // HTML that will become the layer
- _layerId : null, // HTML id attribute
- _layerAttrs : null, // HTML attributes
- _layerClasses : 'ezl layer',// CSS classes
-
- originX : 0,
- originY : 0,
-
- hasCanvas : true, // Whether to create a canvas
- useCanvasScaling : false, // Default to CSS3 scaling
- alwaysClearDrawing : true, // Whether to clear the canvas content before redraw
- alwaysClearAttrs : false, // Whether to remove all canvas attributes (CONTEXT_ATTRS)
- // and transforms (scale, rotate, translate) and reset defaults before redraw
-
-
-
- /// State ///
-
- canvas : null,
- parent : null,
- children : null,
-
- dirty : true,
- ctx : null,
- animActive : null,
- animQueue : null,
- _erased : null,
-
- layerWidth : 0, canvasWidth : 0, realWidth : 0,
- layerHeight : 0, canvasHeight : 0, realHeight : 0,
-
- x: 0, y: 0, loc : null, // Position relative to parent
-
- // Bleeds are marks outside the declared layer-size
- negBleed : null, // Loc
- posBleed : null, // Loc
-
- // Transforms
- _origin : null, // rotational origin
- transform : null, // Object
-
-
- /// Setup ///
+evt.subclass('Layer', {
+ __mixins__ : [ Layerable ],
+ __bind__ : [],
- init : function init(props, attrs, html){
- if (props !== undefined && props !== null) {
- switch (typeof props) {
- case 'boolean' : this.hasCanvas = props; break;
- case 'string' : this._layerHtml = props; break;
- case 'object' : Y.core.extend(this, props); break;
- }
- }
-
- this.children = new Y.YArray();
- this.animActive = new Y.YArray();
- this.animQueue = new Y.YArray();
- this._erased = new Y.YArray();
-
- this.loc = new Loc(0,0);
- this.negBleed = new Loc(0,0);
- this.posBleed = new Loc(0,0);
-
- this.bbox = new BoundingBox(0,0, 0,0, this.originX,this.originY);
- this._origin = this.bbox.origin;
-
- this.transform = {
- rotate : 0,
- scale : new Loc(1.0,1.0),
- translate : new Loc(0,0) // translates canvas
- };
-
- this.layer = jQuery(html || this._layerHtml)
- .addClass(this._layerClasses)
- .data('layer', this);
- this.layer[0].layer = this;
-
- if (this._layerAttrs) this.layer.attr(this._layerAttrs);
- if (this._layerId) this.layer.attr('id', this._layerId);
- if (attrs) this.layer.attr(attrs);
-
- if (this.hasCanvas) {
- this.canvas = jQuery('<canvas />')
- .appendTo(this.layer);
- this.ctx = this.canvas[0].getContext('2d');
- this.canvas[0].layer = this;
- } else {
- this.canvas = jQuery();
- }
-
- // this.tick = this.tick.bind(this);
- },
-
- /// Scene Graph Heirarchy ///
-
- /**
- * @param {Layer} child
- * @return {this}
- */
- append : function append(child){
- new Y(arguments).invoke('appendTo', this);
- return this;
- },
-
- /**
- * @param {Layer} parent
- * @return {this}
- */
- appendTo : function appendTo(parent){
- if (!parent) return this;
-
- // Always ensure we detach from the DOM and redraw this node
- this.remove();
- this.dirty = true;
-
- if ( !(parent instanceof Layer) && (parent.shape instanceof Layer) )
- parent = parent.shape;
-
- // Layer? Add self as new child, fix DOM
- if ( parent instanceof Layer ) {
- this.parent = parent;
- parent.children.push(this);
- parent.layer.append(this.layer);
-
- // Otherwise: Attach to a DOM node as a new root layer node, leave parent null
- } else
- $(parent).append(this.layer);
-
- return this;
- },
-
- /**
- * @param {Layer} child
- * @return {this}
- */
- prepend : function prepend(child){
- new Y(arguments).invoke('prependTo', this);
- return this;
- },
-
- /**
- * @param {Layer} parent
- * @return {this}
- */
- prependTo : function prependTo(parent){
- if (!parent) return this;
-
- // Always ensure we detach from the DOM and redraw this node
- this.remove();
- this.dirty = true;
-
- if ( !(parent instanceof Layer) && (parent.shape instanceof Layer) )
- parent = parent.shape;
-
- // Layer? Add self as new child, fix DOM
- if ( parent instanceof Layer ) {
- this.parent = parent;
- parent.children.unshift(this);
- parent.layer.prepend(this.layer);
-
- // Otherwise: Attach to a DOM node as a new root layer node, leave parent null
- } else
- $(parent).prepend(this.layer);
-
- return this;
- },
-
- /**
- * Removes this layer from its parent and the DOM.
- */
- remove : function remove(){
- if (this.parent)
- this.parent.children.remove(this);
- this.parent = null;
- this.layer.remove();
- return this;
- },
-
- /**
- * Clears this layer and destroys all children.
- */
- empty : function empty(ctx){
- this.children.invoke('remove');
- this.clear(false, ctx);
- return this;
- },
-
- /**
- * @return The root of the scene graph.
- */
- root : function root(){
- if (this.parent)
- return this.parent.root();
- else
- return this;
- },
-
-
-
-
-
- /// Attributes ///
-
- // set : function set(key, value, def){
- // if ( key === undefined || (value === undefined && def === undefined) )
- // return this;
- //
- // value = (value !== undefined ? value : def);
- // if ( FAUX_ACCESSORS.has(key) )
- // this[key](value);
- // else
- // this[key] = value;
- //
- // return this;
- // },
-
- attr : Y.attr.methodize(),
-
- size : function size(w,h){
- if (w === undefined && h === undefined)
- return new Vec(this.layerWidth,this.layerHeight);
-
- if (w === null) w = this.realWidth;
- if (h === null) h = this.realHeight;
-
- this.realWidth = w;
- this.realHeight = h;
-
- // HTMLElement.{width,height} is a long
- this.layerWidth = Math.round(w);
- this.layerHeight = Math.round(h);
-
- var bb = this.bbox.resize(w,h)
- , nb = this.negBleed, pb = this.posBleed
-
- , cw = this.canvasWidth = Math.ceil(w + nb.x + pb.x)
- , ch = this.canvasHeight = Math.ceil(h + nb.y + pb.y)
- ;
-
- this.layer.css({
- 'width' : w, 'height' : h,
- 'left' : bb.x1, 'top' : bb.y1
- });
- this.canvas.css({
- 'width' : cw, 'height' : ch,
- 'margin-left' : -nb.x, 'margin-top' : -nb.y
- });
- var el = this.canvas[0];
- if (el) {
- el.width = cw;
- el.height = ch;
- }
-
- return this;
- },
-
- /**
- * Changes the layer's width and then updates the canvas.
- */
- width : function width(w){
- if (w === undefined)
- return this.layerWidth;
-
- this.layerWidth = w;
-
- var bb = this.bbox.resize(w, this.layerHeight)
- // , ro = bb.relOrigin
- , nb = this.negBleed
- , cw = this.canvasWidth = Math.ceil(w + nb.x + this.posBleed.x); // HTMLCanvas.width is a long
-
- this.layer.css({
- 'width' : w,
- 'left' : bb.x1,
- // 'margin-left' : -ro.x
- });
-
- this.canvas.css({
- 'width' : cw,
- 'margin-left' : -nb.x
- });
- if (this.canvas.length) this.canvas[0].width = cw;
-
- return this;
- },
-
- height : function height(h){
- if (h === undefined)
- return this.layerHeight;
-
- this.layerHeight = h;
-
- var bb = this.bbox.resize(this.layerWidth, h)
- // , ro = bb.relOrigin
- , nb = this.negBleed
- , ch = this.canvasHeight = Math.ceil(h + nb.y + this.posBleed.y); // HTMLCanvas.height is a long
-
- this.layer.css({
- 'height' : h,
- 'top' : bb.y1,
- // 'margin-top' : -ro.y
- });
-
- this.canvas.css({
- 'height' : ch,
- 'margin-top' : -nb.y
- });
- if (this.canvas.length) this.canvas[0].height = ch;
-
- return this;
- },
-
- /**
- * position() -> {Loc}
- * Gets position of the layer relative to the parent.
- * @return {Loc}
- */
- /**
- * position(x,y) -> {this}
- * Sets the position of this node, and then returns it.
- * @param {Number|String|undefined} x
- * @param {Number|String|undefined} y If omitted, this method must be invoked with `undefined` as the first argument.
- * @return {this}
- */
- /**
- * position(pos) -> {this}
- * Sets the position of this node, and then returns it.
- * @param {Object} pos An object with "x" and/or "y" properties as in `position(x,y)`.
- * @return {this}
- */
- position : function position(x,y){
- if (x === undefined && y === undefined)
- return this.layer.position();
-
- if ( x instanceof Array ) {
- y = x[_Y]; x = x[_X];
- } else if ( Y.isPlainObject(x) ){
- y = ('top' in x ? x.top : x.y);
- x = ('left' in x ? x.left : x.x);
- }
-
- var bbox = this.bbox.relocate(x,y);
- this.css({
- 'left' : bbox.x1,
- 'top' : bbox.y1
- });
- return this;
- },
-
- stroke : function stroke(style, width){
- if (style === undefined && width === undefined)
- return this.strokeStyle;
-
- if (width !== undefined)
- this.lineWidth = width;
-
- this.dirty = true;
- this.strokeStyle = style;
- return this;
- },
- fill : function fill(style){
- if (style === undefined)
- return this.fillStyle;
+ init : function initLayer(props, attrs, html){
+ this.setup(props, attrs, html);
- this.dirty = true;
- this.fillStyle = style;
- return this;
- },
-
- hide : makeDelegate('hide'),
- show : makeDelegate('show'),
-
- /** CSS properties */
- css : makeDelegate('css'),
-
- /** Position relative to document. */
- offset : makeDelegate('offset'),
-
-
-
- /// Transformations ///
-
- /**
- * Gets and sets the origin-location for this shape, used for
- * position and rotation. Defaults to the midpoint.
- */
- origin : function origin(x,y){
- var o = this._origin;
- if (arguments.length === 0)
- return o.absolute(this.layerWidth, this.layerHeight);
-
- if ( x instanceof Array ) {
- y = x[_Y]; x = x[_X];
- }
- o.x = x;
- o.y = y;
- return this._applyTransforms();
- },
-
- /**
- * Rotates this layer by r radians.
- */
- rotate : function rotate(r){
- var t = this.transform;
- if (r === undefined)
- return t.rotate;
-
- t.rotate = r;
- return this._applyTransforms();
- },
-
- /**
- * Scales this layer by (sx,sy), and then applies scaling relatively
- * to all sublayers (preserving knowledge of their individual scaling).
- */
- scale : function scale(sx,sy){
- var ts = this.transform.scale;
- if (arguments.length === 0)
- return ts;
-
- if ( sx instanceof Array ) {
- sy = sx[_Y]; sx = sx[_X];
- }
- ts.x = sx;
- ts.y = sy;
- this.dirty = true;
- return this._applyTransforms();
- },
-
- /**
- * Translates draw calls by (x,y) within this layer only. This allows you to
- * functionally move the origin of the coordinate system for this layer.
- */
- translate : function translate(tx,ty){
- var tt = this.transform.translate;
- if (arguments.length === 0)
- return tt;
-
- if ( tx instanceof Array ) {
- ty = tx[_Y]; tx = tx[_X];
- }
- tt.x = tx;
- tt.y = ty;
- this.dirty = true;
- return this;
- },
-
- /**
- * @private
- */
- _applyTransforms : function _applyTransforms(){
- var t = this.transform, tfns = [];
-
- if (t.rotate !== 0)
- tfns.push('rotate('+t.rotate+'rad)');
-
- if (!this.useCanvasScaling && (t.scale.x !== 1 || t.scale.y !== 1))
- tfns.push('scale('+t.scale.x+','+t.scale.y+')');
-
- var trans = (tfns.length ? tfns.join(' ') : 'none')
- , o = this._origin.toUnits('px')
- , origin = o.x+' '+o.y ;
- this.layer.css(['', '-moz-', '-webkit-']
- .reduce(
- function(values, prefix){
- values[prefix+'transform'] = trans;
- values[prefix+'transform-origin'] = origin;
- return values;
- },
- {}) );
- return this;
- },
-
-
-
-
-
-
-
- /// Iterators ///
-
- invoke : function invoke(name){
- var args = Y(arguments,1);
- // this[name].apply(this, args);
- // this.children.invoke.apply(this.children, ['invoke', name].concat(args));
- return this._invoke(name, args);
- },
- _invoke : function _invoke(name, args){
- this[name].apply(this, args);
- this.children.invoke('_invoke', name, args);
- return this;
- },
-
- setAll : function setAll(k,v){
- this[k] = v;
- this.children.invoke('setAll', k,v);
- return this;
- },
-
- /**
- * Reduce "up", across this and parents, inner to outer:
- * acc = fn.call(context || node, acc, node)
- */
- reduceup : function reduceup(fn, acc, context){
- acc = fn.call(context || this, acc, this);
- return ( this.parent ? this.parent.reduceup(fn, acc, context) : acc );
- },
-
- /**
- * Reduce "down", across this and children, depth-first:
- * acc = fn.call(context || node, acc, node)
- */
- reduce : function reduce(fn, acc, context){
- acc = fn.call(context || this, acc, this);
- return this.children.reduce(function(acc, child){
- return child.reduce(fn, acc, context);
- }, acc);
- },
-
-
-
-
-
-
- /// Drawing Functions ///
-
- /**
- * @param {CanvasDrawingContext2D} [ctx=this.ctx] Forces context to use rather than the layer's own.
- * @param {Boolean} [force=false] Forces redraw.
- */
- draw : function draw(ctx, force){
- var _ctx = ctx || this.ctx;
-
- if ( this.dirty || force ){
- this.dirty = false;
- this._openPath(_ctx);
- this.render(_ctx);
- this._closePath(_ctx);
- if (_ctx) this._erased.forEach(this._erase, this);
- }
-
- this.children.invoke('draw', ctx, force);
- return this;
- },
-
- _openPath : function _openPath(ctx){
- if (!ctx) return this;
-
- var self = this
- , neg = this.negBleed
- , t = this.transform
- , w = this.canvasWidth, h = this.canvasHeight ;
-
- ctx.beginPath();
-
- // TODO: only alwaysClearAttrs should reset transforms?
- // if (this.alwaysClearAttrs)
- ctx.setTransform(1,0,0,1,0,0);
-
- if (this.alwaysClearDrawing)
- ctx.clearRect(-w,-h, 2*w,2*h);
-
- // if (this.alwaysClearAttrs) {
- if (this.useCanvasScaling && (t.scale.x !== 1 || t.scale.y !== 1))
- ctx.scale(t.scale.x,t.scale.y);
-
- ctx.translate(neg.x, neg.y);
- ctx.translate(t.translate.x, t.translate.y);
- // }
-
- // Set context attributes
- CONTEXT_ATTRS.forEach(function(name){
- if (this[name] !== undefined)
- ctx[name] = this[name];
- else if (this.alwaysClearAttrs)
- delete ctx[name];
- }, this);
-
- return this;
- },
-
- /** To be implemented by subclasses. */
- render : function render(ctx){ return this; },
-
- _closePath : function _closePath(ctx){
- if (ctx) ctx.closePath();
- return this;
- },
-
- erase : function erase(x,y, w,h, alsoChildren){
- this._erased.push({ 'x':x,'y':y, 'w':w,'h':h, 'alsoChildren':alsoChildren });
- this.dirty = true;
- if (alsoChildren)
- this.children.invoke('erase', x,y, w,h, alsoChildren);
- return this;
- },
-
- _erase : function _erase(args){
- var x = args.x, y = args.y
- , w = args.w, h = args.h;
-
- if (w < 0) w = this.canvas.width() + w;
- if (h < 0) h = this.canvas.height() + h;
-
- this.ctx.beginPath();
- this.ctx.clearRect(x,y, w,h);
- this.ctx.closePath();
- },
-
- /**
- * Clears this layer and optionally all children.
- */
- clear : function clear(alsoChildren, ctx){
- ctx = ctx || this.ctx;
-
- if (ctx) {
- var w = this.canvas.width()
- , h = this.canvas.height();
- ctx.beginPath();
- ctx.setTransform(1,0,0,1,0,0);
- ctx.clearRect(-w,-h, 2*w,2*h);
- ctx.closePath();
- }
-
- if (alsoChildren)
- this.children.invoke('clear');
-
- return this;
- },
-
- /**
- * @param {Number} duration Duration (ms) of animation.
- * @param {Object} props Object whose keys are the property-names to animate
- * paired with the target value. Animated properties can also be relative.
- * If a value is supplied with a leading += or -= sequence of characters,
- * then the target value is computed by adding or subtracting the given
- * number from the current value of the property.
- * @param {Object} [options] Animation options:
- * * {Number} [delay=0] Milliseconds to wait before beginning animation.
- * * {Boolean} [queue=true] Indicates whether to place the animation
- * in the effects queue to run when other animations have finished.
- * If false, the animation will begin after `delay` milliseconds;
- * likewise, if `delay` is non-zero `queue` defaults to `false`
- * instead.
- * * {String|Function|Object} [easing='linear'] Name of easing function
- * used to calculate each step value, or a function, or a map of
- * properties to either of the above.
- * * {Function} [complete] Function called on animation completion.
- * * {Function} [step] Function called after each step of the animation.
- * @param {Float} [now] Time (ms) to use as the start-time. If omitted, the
- * current time is used.
- */
- animate : function animate(duration, props, options, now) {
- var A = new Animation(this, duration, props, options);
- if ( A.options.queue && this.animActive.length )
- this.animQueue.push(A);
- else
- this.animActive.push(A.start(now || new Date().getTime()));
- return this;
- },
-
- tick : function tick(elapsed, now){
- if (elapsed instanceof Event) {
- var d = evt.data;
- now = d.now;
- elapsed = d.elapsed;
- }
-
- this.animActive = this.animActive.filter(function(anim){
- return anim.tick(elapsed, now);
- });
-
- var childrenRunning = this.children.invoke('tick', elapsed, now).some(op.I)
- , running = !!this.animActive.length;
- if ( !running && this.animQueue.length )
- this.animActive.push( this.animQueue.shift().start(now) );
-
- return childrenRunning || running;
- },
-
- /// Debuggging ///
-
- // for debugging
- point : function point(x,y, color){
- var ctx = this.ctx;
- // this._openPath(ctx);
-
- var r = 2;
- ctx.beginPath();
- ctx.arc(x,y, r, 0, Math.PI*2, false);
- ctx.fillStyle = color || '#FFFFFF';
- ctx.fill();
- ctx.closePath();
-
- // this._closePath(ctx);
- return this;
- },
-
- title : function title(val){
- if (val === undefined)
- return this.layer.attr('title');
-
- this.layer.attr('title', val);
- return this;
- },
-
-
-
- /// Misc ///
- toString : function(){
- var pos = ((this.layer.length && this.layer.parent()[0]) ? this.position() : {top:NaN, left:NaN});
- return this.className+'['+pos.left+','+pos.top+']( children='+this.children.size()+' )';
}
-});
-
-function makeDelegate(name, dirties, prop){
- prop = prop || 'layer';
- return function delegate(){
- if (dirties && arguments.length)
- this.dirty = true;
-
- var target = this[prop]
- , result = target[name].apply(target, arguments);
-
- return (result !== target ? result : this);
- };
-}
-
-// Install CSS styles
-$(function(){
- $('<style />')
- .text([
- '.ezl.layer { position:absolute; z-index:1; top:0; left:0; line-height:0; }',
- // '.ezl.layer { outline:1px solid #000000; }',
- '.ezl.layer canvas { z-index:0; }'
- ].join('\n'))
- .appendTo('head');
-});
+
+})
+;
--- /dev/null
+//#ensure "jquery"
+var Y = require('Y').Y
+, op = require('Y/op')
+
+, Mixin = require('evt').Mixin
+
+, Vec = require('ezl/math/vec').Vec
+, Loc = require('ezl/loc/loc').Loc
+, BoundingBox = require('ezl/loc/boundingbox').BoundingBox
+, Animation = require('ezl/loop/fx').Animation
+,
+
+CONTEXT_ATTRS = Y('globalAlpha globalCompositeOperation strokeStyle fillStyle lineWidth lineCap lineJoin miterLimit shadowOffsetX shadowOffsetY shadowBlur shadowColor'.split(' ')),
+FAUX_ACCESSORS = Y('width height position stroke fill origin rotate scale translate title'.split(' '))
+, _X = 0, _Y = 1
+,
+
+
+
+Layerable =
+exports['Layerable'] =
+Mixin.subclass('Layerable', {
+ __bind__ : [ 'tick' ],
+ __layer__ : true,
+
+ /// Class Defaults ///
+
+ // _layer* is applied to this.layer (the DOM Element)
+ _layerHtml : '<div/>', // HTML that will become the layer
+ _layerId : null, // HTML id attribute
+ _layerAttrs : null, // HTML attributes
+ _layerClasses : 'ezl layer',// CSS classes
+
+ originX : 0,
+ originY : 0,
+
+ hasCanvas : true, // Whether to create a canvas
+ useCanvasScaling : false, // Default to CSS3 scaling
+ alwaysClearDrawing : true, // Whether to clear the canvas content before redraw
+ alwaysClearAttrs : false, // Whether to remove all canvas attributes (CONTEXT_ATTRS)
+ // and transforms (scale, rotate, translate) and reset defaults before redraw
+
+
+
+ /// State ///
+
+ canvas : null,
+ parent : null,
+ children : null,
+
+ dirty : true,
+ ctx : null,
+ animActive : null,
+ animQueue : null,
+ _erased : null,
+
+ layerWidth : 0, canvasWidth : 0, realWidth : 0,
+ layerHeight : 0, canvasHeight : 0, realHeight : 0,
+
+ x: 0, y: 0, loc : null, // Position relative to parent
+
+ // Bleeds are marks outside the declared layer-size
+ negBleed : null, // Loc
+ posBleed : null, // Loc
+
+ // Transforms
+ _origin : null, // rotational origin
+ transform : null, // Object
+
+
+ /// Setup ///
+
+ setup : function setupLayer(props, attrs, html){
+ if (props !== undefined && props !== null) {
+ switch (typeof props) {
+ case 'boolean' : this.hasCanvas = props; break;
+ case 'string' : this._layerHtml = props; break;
+ case 'object' : Y.core.extend(this, props); break;
+ }
+ }
+
+ this.children = new Y.YArray();
+ this.animActive = new Y.YArray();
+ this.animQueue = new Y.YArray();
+ this._erased = new Y.YArray();
+
+ this.loc = new Loc(0,0);
+ this.negBleed = new Loc(0,0);
+ this.posBleed = new Loc(0,0);
+
+ this.bbox = new BoundingBox(0,0, 0,0, this.originX,this.originY);
+ this._origin = this.bbox.origin;
+
+ this.transform = {
+ rotate : 0,
+ scale : new Loc(1.0,1.0),
+ translate : new Loc(0,0) // translates canvas
+ };
+
+ this.layer = jQuery(html || this._layerHtml)
+ .addClass(this._layerClasses)
+ .data('layer', this);
+ this.layer[0].layer = this;
+
+ if (this._layerAttrs) this.layer.attr(this._layerAttrs);
+ if (this._layerId) this.layer.attr('id', this._layerId);
+ if (attrs) this.layer.attr(attrs);
+
+ if (this.hasCanvas) {
+ this.canvas = jQuery('<canvas />')
+ .appendTo(this.layer);
+ this.ctx = this.canvas[0].getContext('2d');
+ this.canvas[0].layer = this;
+ } else {
+ this.canvas = jQuery();
+ }
+
+ // this.tick = this.tick.bind(this);
+ },
+
+ /// Scene Graph Heirarchy ///
+
+ /**
+ * @param {Layerable} child
+ * @return {this}
+ */
+ append : function append(child){
+ new Y(arguments).invoke('appendTo', this);
+ return this;
+ },
+
+ /**
+ * @param {Layerable} parent
+ * @return {this}
+ */
+ appendTo : function appendTo(parent){
+ if (!parent) return this;
+
+ // Always ensure we detach from the DOM and redraw this node
+ this.remove();
+ this.dirty = true;
+
+ if ( !parent.__layer__ && parent.shape && parent.shape.__layer__ )
+ parent = parent.shape;
+
+ // Layer? Add self as new child, fix DOM
+ if ( parent.__layer__ ) {
+ this.parent = parent;
+ parent.children.push(this);
+ parent.layer.append(this.layer);
+
+ // Otherwise: Attach to a DOM node as a new root layer node, leave parent null
+ } else
+ $(parent).append(this.layer);
+
+ return this;
+ },
+
+ /**
+ * @param {Layerable} child
+ * @return {this}
+ */
+ prepend : function prepend(child){
+ new Y(arguments).invoke('prependTo', this);
+ return this;
+ },
+
+ /**
+ * @param {Layerable} parent
+ * @return {this}
+ */
+ prependTo : function prependTo(parent){
+ if (!parent) return this;
+
+ // Always ensure we detach from the DOM and redraw this node
+ this.remove();
+ this.dirty = true;
+
+ if ( !parent.__layer__ && parent.shape && parent.shape.__layer__ )
+ parent = parent.shape;
+
+ // Layer? Add self as new child, fix DOM
+ if ( parent.__layer__ ) {
+ this.parent = parent;
+ parent.children.unshift(this);
+ parent.layer.prepend(this.layer);
+
+ // Otherwise: Attach to a DOM node as a new root layer node, leave parent null
+ } else
+ $(parent).prepend(this.layer);
+
+ return this;
+ },
+
+ /**
+ * Removes this layer from its parent and the DOM.
+ */
+ remove : function remove(){
+ if (this.parent)
+ this.parent.children.remove(this);
+ this.parent = null;
+ this.layer.remove();
+ return this;
+ },
+
+ /**
+ * Clears this layer and destroys all children.
+ */
+ empty : function empty(ctx){
+ this.children.invoke('remove');
+ this.clear(false, ctx);
+ return this;
+ },
+
+ /**
+ * @return The root of the scene graph.
+ */
+ root : function root(){
+ if (this.parent)
+ return this.parent.root();
+ else
+ return this;
+ },
+
+
+
+
+
+ /// Attributes ///
+
+ // set : function set(key, value, def){
+ // if ( key === undefined || (value === undefined && def === undefined) )
+ // return this;
+ //
+ // value = (value !== undefined ? value : def);
+ // if ( FAUX_ACCESSORS.has(key) )
+ // this[key](value);
+ // else
+ // this[key] = value;
+ //
+ // return this;
+ // },
+
+ attr : Y.attr.methodize(),
+
+ size : function size(w,h){
+ if (w === undefined && h === undefined)
+ return new Vec(this.layerWidth,this.layerHeight);
+
+ if (w === null) w = this.realWidth;
+ if (h === null) h = this.realHeight;
+
+ this.realWidth = w;
+ this.realHeight = h;
+
+ // HTMLElement.{width,height} is a long
+ this.layerWidth = Math.round(w);
+ this.layerHeight = Math.round(h);
+
+ var bb = this.bbox.resize(w,h)
+ , nb = this.negBleed, pb = this.posBleed
+
+ , cw = this.canvasWidth = Math.ceil(w + nb.x + pb.x)
+ , ch = this.canvasHeight = Math.ceil(h + nb.y + pb.y)
+ ;
+
+ this.layer.css({
+ 'width' : w, 'height' : h,
+ 'left' : bb.x1, 'top' : bb.y1
+ });
+ this.canvas.css({
+ 'width' : cw, 'height' : ch,
+ 'margin-left' : -nb.x, 'margin-top' : -nb.y
+ });
+ var el = this.canvas[0];
+ if (el) {
+ el.width = cw;
+ el.height = ch;
+ }
+
+ return this;
+ },
+
+ /**
+ * Changes the layer's width and then updates the canvas.
+ */
+ width : function width(w){
+ if (w === undefined)
+ return this.layerWidth;
+
+ this.layerWidth = w;
+
+ var bb = this.bbox.resize(w, this.layerHeight)
+ // , ro = bb.relOrigin
+ , nb = this.negBleed
+ , cw = this.canvasWidth = Math.ceil(w + nb.x + this.posBleed.x); // HTMLCanvas.width is a long
+
+ this.layer.css({
+ 'width' : w,
+ 'left' : bb.x1,
+ // 'margin-left' : -ro.x
+ });
+
+ this.canvas.css({
+ 'width' : cw,
+ 'margin-left' : -nb.x
+ });
+ if (this.canvas.length) this.canvas[0].width = cw;
+
+ return this;
+ },
+
+ height : function height(h){
+ if (h === undefined)
+ return this.layerHeight;
+
+ this.layerHeight = h;
+
+ var bb = this.bbox.resize(this.layerWidth, h)
+ // , ro = bb.relOrigin
+ , nb = this.negBleed
+ , ch = this.canvasHeight = Math.ceil(h + nb.y + this.posBleed.y); // HTMLCanvas.height is a long
+
+ this.layer.css({
+ 'height' : h,
+ 'top' : bb.y1,
+ // 'margin-top' : -ro.y
+ });
+
+ this.canvas.css({
+ 'height' : ch,
+ 'margin-top' : -nb.y
+ });
+ if (this.canvas.length) this.canvas[0].height = ch;
+
+ return this;
+ },
+
+ /**
+ * position() -> {Loc}
+ * Gets position of the layer relative to the parent.
+ * @return {Loc}
+ */
+ /**
+ * position(x,y) -> {this}
+ * Sets the position of this node, and then returns it.
+ * @param {Number|String|undefined} x
+ * @param {Number|String|undefined} y If omitted, this method must be invoked with `undefined` as the first argument.
+ * @return {this}
+ */
+ /**
+ * position(pos) -> {this}
+ * Sets the position of this node, and then returns it.
+ * @param {Object} pos An object with "x" and/or "y" properties as in `position(x,y)`.
+ * @return {this}
+ */
+ position : function position(x,y){
+ if (x === undefined && y === undefined)
+ return this.layer.position();
+
+ if ( x instanceof Array ) {
+ y = x[_Y]; x = x[_X];
+ } else if ( Y.isPlainObject(x) ){
+ y = ('top' in x ? x.top : x.y);
+ x = ('left' in x ? x.left : x.x);
+ }
+
+ var bbox = this.bbox.relocate(x,y);
+ this.css({
+ 'left' : bbox.x1,
+ 'top' : bbox.y1
+ });
+ return this;
+ },
+
+ stroke : function stroke(style, width){
+ if (style === undefined && width === undefined)
+ return this.strokeStyle;
+
+ if (width !== undefined)
+ this.lineWidth = width;
+
+ this.dirty = true;
+ this.strokeStyle = style;
+ return this;
+ },
+
+ fill : function fill(style){
+ if (style === undefined)
+ return this.fillStyle;
+
+ this.dirty = true;
+ this.fillStyle = style;
+ return this;
+ },
+
+ hide : makeDelegate('hide'),
+ show : makeDelegate('show'),
+ toggle : makeDelegate('toggle'),
+
+ /** CSS properties */
+ css : makeDelegate('css'),
+
+ /** Position relative to document. */
+ offset : makeDelegate('offset'),
+
+
+
+ /// Transformations ///
+
+ /**
+ * Gets and sets the origin-location for this shape, used for
+ * position and rotation. Defaults to the midpoint.
+ */
+ origin : function origin(x,y){
+ var o = this._origin;
+ if (arguments.length === 0)
+ return o.absolute(this.layerWidth, this.layerHeight);
+
+ if ( x instanceof Array ) {
+ y = x[_Y]; x = x[_X];
+ }
+ o.x = x;
+ o.y = y;
+ return this._applyTransforms();
+ },
+
+ /**
+ * Rotates this layer by r radians.
+ */
+ rotate : function rotate(r){
+ var t = this.transform;
+ if (r === undefined)
+ return t.rotate;
+
+ t.rotate = r;
+ return this._applyTransforms();
+ },
+
+ /**
+ * Scales this layer by (sx,sy), and then applies scaling relatively
+ * to all sublayers (preserving knowledge of their individual scaling).
+ */
+ scale : function scale(sx,sy){
+ var ts = this.transform.scale;
+ if (arguments.length === 0)
+ return ts;
+
+ if ( sx instanceof Array ) {
+ sy = sx[_Y]; sx = sx[_X];
+ }
+ ts.x = sx;
+ ts.y = sy;
+ this.dirty = true;
+ return this._applyTransforms();
+ },
+
+ /**
+ * Translates draw calls by (x,y) within this layer only. This allows you to
+ * functionally move the origin of the coordinate system for this layer.
+ */
+ translate : function translate(tx,ty){
+ var tt = this.transform.translate;
+ if (arguments.length === 0)
+ return tt;
+
+ if ( tx instanceof Array ) {
+ ty = tx[_Y]; tx = tx[_X];
+ }
+ tt.x = tx;
+ tt.y = ty;
+ this.dirty = true;
+ return this;
+ },
+
+ /**
+ * @private
+ */
+ _applyTransforms : function _applyTransforms(){
+ var t = this.transform, tfns = [];
+
+ if (t.rotate !== 0)
+ tfns.push('rotate('+t.rotate+'rad)');
+
+ if (!this.useCanvasScaling && (t.scale.x !== 1 || t.scale.y !== 1))
+ tfns.push('scale('+t.scale.x+','+t.scale.y+')');
+
+ var trans = (tfns.length ? tfns.join(' ') : 'none')
+ , o = this._origin.toUnits('px')
+ , origin = o.x+' '+o.y ;
+ this.layer.css(['', '-moz-', '-webkit-']
+ .reduce(
+ function(values, prefix){
+ values[prefix+'transform'] = trans;
+ values[prefix+'transform-origin'] = origin;
+ return values;
+ },
+ {}) );
+ return this;
+ },
+
+
+
+
+
+
+
+ /// Iterators ///
+
+ invoke : function invoke(name){
+ var args = Y(arguments,1);
+ // this[name].apply(this, args);
+ // this.children.invoke.apply(this.children, ['invoke', name].concat(args));
+ return this._invoke(name, args);
+ },
+ _invoke : function _invoke(name, args){
+ this[name].apply(this, args);
+ this.children.invoke('_invoke', name, args);
+ return this;
+ },
+
+ setAll : function setAll(k,v){
+ this[k] = v;
+ this.children.invoke('setAll', k,v);
+ return this;
+ },
+
+ /**
+ * Reduce "up", across this and parents, inner to outer:
+ * acc = fn.call(context || node, acc, node)
+ */
+ reduceup : function reduceup(fn, acc, context){
+ acc = fn.call(context || this, acc, this);
+ return ( this.parent ? this.parent.reduceup(fn, acc, context) : acc );
+ },
+
+ /**
+ * Reduce "down", across this and children, depth-first:
+ * acc = fn.call(context || node, acc, node)
+ */
+ reduce : function reduce(fn, acc, context){
+ acc = fn.call(context || this, acc, this);
+ return this.children.reduce(function(acc, child){
+ return child.reduce(fn, acc, context);
+ }, acc);
+ },
+
+
+
+
+
+
+ /// Drawing Functions ///
+
+ /**
+ * @param {CanvasDrawingContext2D} [ctx=this.ctx] Forces context to use rather than the layer's own.
+ * @param {Boolean} [force=false] Forces redraw.
+ */
+ draw : function draw(ctx, force){
+ var _ctx = ctx || this.ctx;
+
+ if ( this.dirty || force ){
+ this.dirty = false;
+ this._openPath(_ctx);
+ this.render(_ctx);
+ this._closePath(_ctx);
+ if (_ctx) this._erased.forEach(this._erase, this);
+ }
+
+ this.children.invoke('draw', ctx, force);
+ return this;
+ },
+
+ _openPath : function _openPath(ctx){
+ if (!ctx) return this;
+
+ var self = this
+ , neg = this.negBleed
+ , t = this.transform
+ , w = this.canvasWidth, h = this.canvasHeight ;
+
+ ctx.beginPath();
+
+ // TODO: only alwaysClearAttrs should reset transforms?
+ // if (this.alwaysClearAttrs)
+ ctx.setTransform(1,0,0,1,0,0);
+
+ if (this.alwaysClearDrawing)
+ ctx.clearRect(-w,-h, 2*w,2*h);
+
+ // if (this.alwaysClearAttrs) {
+ if (this.useCanvasScaling && (t.scale.x !== 1 || t.scale.y !== 1))
+ ctx.scale(t.scale.x,t.scale.y);
+
+ ctx.translate(neg.x, neg.y);
+ ctx.translate(t.translate.x, t.translate.y);
+ // }
+
+ // Set context attributes
+ CONTEXT_ATTRS.forEach(function(name){
+ if (this[name] !== undefined)
+ ctx[name] = this[name];
+ else if (this.alwaysClearAttrs)
+ delete ctx[name];
+ }, this);
+
+ return this;
+ },
+
+ /** To be implemented by subclasses. */
+ render : function render(ctx){ return this; },
+
+ _closePath : function _closePath(ctx){
+ if (ctx) ctx.closePath();
+ return this;
+ },
+
+ erase : function erase(x,y, w,h, alsoChildren){
+ this._erased.push({ 'x':x,'y':y, 'w':w,'h':h, 'alsoChildren':alsoChildren });
+ this.dirty = true;
+ if (alsoChildren)
+ this.children.invoke('erase', x,y, w,h, alsoChildren);
+ return this;
+ },
+
+ _erase : function _erase(args){
+ var x = args.x, y = args.y
+ , w = args.w, h = args.h;
+
+ if (w < 0) w = this.canvas.width() + w;
+ if (h < 0) h = this.canvas.height() + h;
+
+ this.ctx.beginPath();
+ this.ctx.clearRect(x,y, w,h);
+ this.ctx.closePath();
+ },
+
+ /**
+ * Clears this layer and optionally all children.
+ */
+ clear : function clear(alsoChildren, ctx){
+ ctx = ctx || this.ctx;
+
+ if (ctx) {
+ var w = this.canvas.width()
+ , h = this.canvas.height();
+ ctx.beginPath();
+ ctx.setTransform(1,0,0,1,0,0);
+ ctx.clearRect(-w,-h, 2*w,2*h);
+ ctx.closePath();
+ }
+
+ if (alsoChildren)
+ this.children.invoke('clear');
+
+ return this;
+ },
+
+ /**
+ * @param {Number} duration Duration (ms) of animation.
+ * @param {Object} props Object whose keys are the property-names to animate
+ * paired with the target value. Animated properties can also be relative.
+ * If a value is supplied with a leading += or -= sequence of characters,
+ * then the target value is computed by adding or subtracting the given
+ * number from the current value of the property.
+ * @param {Object} [options] Animation options:
+ * * {Number} [delay=0] Milliseconds to wait before beginning animation.
+ * * {Boolean} [queue=true] Indicates whether to place the animation
+ * in the effects queue to run when other animations have finished.
+ * If false, the animation will begin after `delay` milliseconds;
+ * likewise, if `delay` is non-zero `queue` defaults to `false`
+ * instead.
+ * * {String|Function|Object} [easing='linear'] Name of easing function
+ * used to calculate each step value, or a function, or a map of
+ * properties to either of the above.
+ * * {Function} [complete] Function called on animation completion.
+ * * {Function} [step] Function called after each step of the animation.
+ * @param {Float} [now] Time (ms) to use as the start-time. If omitted, the
+ * current time is used.
+ */
+ animate : function animate(duration, props, options, now) {
+ var A = new Animation(this, duration, props, options);
+ if ( A.options.queue && this.animActive.length )
+ this.animQueue.push(A);
+ else
+ this.animActive.push(A.start(now || new Date().getTime()));
+ return this;
+ },
+
+ tick : function tick(elapsed, now){
+ if (elapsed instanceof Event) {
+ var d = evt.data;
+ now = d.now;
+ elapsed = d.elapsed;
+ }
+
+ this.animActive = this.animActive.filter(function(anim){
+ return anim.tick(elapsed, now);
+ });
+
+ var childrenRunning = this.children.invoke('tick', elapsed, now).some(op.I)
+ , running = !!this.animActive.length;
+ if ( !running && this.animQueue.length )
+ this.animActive.push( this.animQueue.shift().start(now) );
+
+ return childrenRunning || running;
+ },
+
+ /// Debuggging ///
+
+ // for debugging
+ point : function point(x,y, color){
+ var ctx = this.ctx;
+ // this._openPath(ctx);
+
+ var r = 2;
+ ctx.beginPath();
+ ctx.arc(x,y, r, 0, Math.PI*2, false);
+ ctx.fillStyle = color || '#FFFFFF';
+ ctx.fill();
+ ctx.closePath();
+
+ // this._closePath(ctx);
+ return this;
+ },
+
+ title : function title(val){
+ if (val === undefined)
+ return this.layer.attr('title');
+
+ this.layer.attr('title', val);
+ return this;
+ },
+
+
+
+ /// Misc ///
+ toString : function(){
+ var pos = ((this.layer.length && this.layer.parent()[0]) ? this.position() : {top:NaN, left:NaN});
+ return this.className+'['+pos.left+','+pos.top+']( children='+this.children.size()+' )';
+ }
+});
+
+function makeDelegate(name, dirties, prop){
+ prop = prop || 'layer';
+ return function delegate(){
+ if (dirties && arguments.length)
+ this.dirty = true;
+
+ var target = this[prop]
+ , result = target[name].apply(target, arguments);
+
+ return (result !== target ? result : this);
+ };
+}
+
+// Install CSS styles
+$(function(){
+ $('<style />')
+ .text([
+ '.ezl.layer { position:absolute; z-index:1; top:0; left:0; line-height:0; }',
+ // '.ezl.layer { outline:1px solid #000000; }',
+ '.ezl.layer canvas { z-index:0; }'
+ ].join('\n'))
+ .appendTo('head');
+});
var Y = require('Y').Y
, Event = require('Y/modules/y.event').Event
+, evt = require('evt')
+
, EventLoop = require('ezl/loop').EventLoop
, Layer = require('ezl/layer/layer').Layer
, Circle = require('ezl/shape').Circle
Game =
exports['Game'] =
-Y.subclass('Game', {
+evt.subclass('Game', {
// Config
gameoverDelay : null,
playerAlign : 1,
- init : function initGame(levelId, replayFile){
+ init : function initGame(levelId){
Y.bindAll(this);
if (levelId) this.levelId = levelId;
this.viewport.append(this.map.ui);
// automatically keep track of units
+ Thing.on('create', this.setGame);
Thing.on('created', this.addUnit);
Thing.on('destroy', this.killUnit);
- this.on('tick', this.tick);
+ this.on('tick', this.tick);
this.level.setup();
- if (this.player) {
- this.backpack = new Backpack(this.player)
- .appendTo( this.root )
- .refresh();
- }
- if (replayFile) {
- this.isReplay = true;
- this.loadLog(replayFile);
- } else
- this.emit('ready', this);
+ // if (this.player) {
+ // this.backpack = new Backpack(this.player)
+ // .appendTo( this.root )
+ // .refresh();
+ // }
+
+ this.emit('ready', this);
},
destroy : function destroy(){
this.stop();
- this.player.destroy();
+ if (this.player) this.player.destroy();
+ Thing.removeListener('create', this.noteUnit);
Thing.removeListener('created', this.addUnit);
Thing.removeListener('destroy', this.killUnit);
this.root.remove();
draw : function draw(){
this.root.draw();
- // this.backpack.draw();
},
/**
return animation;
},
+ setGame : function setGame(unit){
+ if (unit instanceof Event)
+ unit = unit.data.instance;
+
+ unit.game = this;
+ return unit;
+ },
+
noteUnit : function noteUnit(unit){
+ if (unit instanceof Event)
+ unit = unit.trigger;
+
unit.game = this;
if (unit.dead) return unit;
, FRAME_RATE = 30
, MS_PER_FRAME = 1000 / FRAME_RATE
-, NOW = 0 // Current tick's timestamp (ms)
-, ELAPSED = MS_PER_FRAME // Time (ms) since previous tick
-, TICKS = 0 // Ticks since start of game
+, NOW = 0 // Current tick's timestamp (ms)
+, ELAPSED = MS_PER_FRAME // Time (ms) since previous tick
+, TICKS = 0 // Ticks since start of game
+, DROPPED = 0 // Frames dropped since last tick
/// Common Components of Computation ///
'effects' : require('tanks/effects'),
'fx' : require('tanks/fx'),
'mixins' : require('tanks/mixins'),
+ 'inventory' : require('tanks/inventory'),
'map' : require('tanks/map'),
'thing' : require('tanks/thing'),
'ui' : require('tanks/ui'),
+var Y = require('Y')
+
+, evt = require('evt')
+
+, Item = require('tanks/thing/item').Item
+, Container = require('tanks/inventory/container').Container
+, ContainerUI = require('tanks/ui/inventory/containerui').ContainerUI
+,
+
+Bag =
+exports['Bag'] =
+Item.subclass('Bag', {
+ __mixins__ : [ Container ],
+ __bind__ : [ 'onAcquired', 'onLost' ],
+
+ /// Setup ///
+ hasUI : true,
+ highlightIconOnOpen : true, // Whether to highlight the inventory icon when bag is open
+ reqs : null,
+
+ /// Bookkeeping ///
+ bagName : null,
+ ui : null, // ContainerUI
+
+
+ init : function initBag(bagName, owner){
+ this.bagName = bagName;
+ this.owner = owner;
+ Item.init.call(this);
+ },
+
+
+ /**
+ * @return {this}
+ */
+ showUI : function showUI(){
+ if (!this.hasUI)
+ return this;
+ if (!this.ui)
+ this.ui = new ContainerUI(this.owner, this, { 'name':this.bagName })
+ .appendTo(this.owner.game.root)
+ .refresh();
+ this.ui.show();
+ return this;
+ },
+
+ hideUI : function hideUI(){
+ if (this.ui)
+ this.ui.hide();
+ return this;
+ },
+
+ activate : function activate(){
+ if (this.ui && this.owner)
+ this.ui.toggle();
+ return this;
+ },
+
+ onAcquired : function onAcquired(evt, container){
+ Item.fn.onAcquired.apply(this, arguments);
+ // this.parent = container;
+ this.showUI();
+ },
+
+ onLost : function onLost(evt){
+ var lostItem = evt.data.item
+ , unit = this.owner
+ ;
+ // Bags are Items AND Containers, and are thus sent notifications when
+ // their contents change.
+ if (lostItem !== this)
+ return;
+
+ this.hideUI();
+ Item.fn.onLost.apply(this, arguments);
+ }
+
+})
+;
+var Y = require('Y')
+, evt = require('evt')
+, Bag = require('tanks/inventory/bag').Bag
+,
+
+/**
+ * Special Bag which holds your backpack (and potentially other bags).
+ */
+BagBag =
+exports['BagBag'] =
+Bag.subclass('BagBag', {
+ __mixins__ : [],
+ __bind__ : [],
+ __static__ : {},
+
+ /// Setup ///
+ hasUI : true,
+ highlightIconOnOpen : true, // Whether to highlight the inventory icon when bag is open
+ reqs : 'bag',
+
+ /// Bookkeeping ///
+ bagName : 'bagbag',
+ ui : null, // ContainerUI
+
+
+ init : function initBagBag(inventory, player){
+ this.inventory = inventory;
+ Bag.init.call(this, 'bagbag', player);
+ this.showUI();
+ if (this.ui) this.ui.layer.addClass('bagbag bag');
+ }
+
+})
+;
+var Y = require('Y')
+, evt = require('evt')
+, Bag = require('tanks/inventory/bag').Bag
+,
+
+/**
+ * A virtual container for item shortcuts.
+ */
+Belt =
+exports['Belt'] =
+Bag.subclass('Belt', {
+ __mixins__ : [],
+ __bind__ : [],
+ __static__ : {},
+
+
+ init : function initBelt(){
+ Bag.init.call(this);
+
+ }
+
+})
+;
, op = require('Y/op')
, deepcopy = require('Y/types/object').deepcopy
-, Mixin = require('evt').Mixin
-, Item = require('tanks/thing/item').Item
+, evt = require('evt')
+, Mixin = evt.Mixin
, kNull = op.K(null)
,
exports['Container'] =
Mixin.subclass('Container', {
__bind__ : [],
-
+ isItemContainer : true, // Instance-of flag
max : 1, // Container capacity
- reqs : null, // Array of tag requirements to be stored here
+ reqs : null, // Array of tag requirements to be stored here // XXX: 'not(bag)'
equipsContents : false, // Whether held items are equipped
owner : null, // Owning unit
children : null, // Nested containers: container id -> Container
- onCreate : function initContainer(evt, owner, items){
- this.owner = owner;
- this.items = {};
- this.children = {};
- this.slots = new Array(this.max);
+ onCreate : function initContainer(evt, self){
+ self.items = {};
+ self.children = {};
+ self.slots = new Array(self.max);
+ if ( typeof self.reqs == 'string' )
+ self.reqs = Y([ self.reqs ]);
+ else
+ self.reqs = Y(self.reqs);
+ },
+
+ setup : function setup(owner, items){
if ( typeof this.reqs == 'string' )
this.reqs = Y([ this.reqs ]);
+ else
+ this.reqs = Y(this.reqs);
+ if (owner) this.owner = owner;
if (items) items.forEach(this.moveItem, this);
+ return this;
},
-
/**
* @param {Item} [item] If present, also check whether item matches this container's requirements.
* @return {Boolean} Whether this container will accept this item.
canAddItem : function canAddItem(item){
var reqs = this.reqs;
return this.size < this.max &&
- ( !item || !reqs || reqs.intersect(item.tags).length === reqs.length );
+ ( !item || !reqs || this.checkReqs(item) );
},
checkReqs : function checkReqs(item){
return !!item && ( !reqs || reqs.intersect(item.tags).length === reqs.length );
},
- getItem : function getItem(idx){
- if ( typeof idx == 'number' ) {
- return this.slots[ (idx < 0 ? this.max+idx : idx) ];
- } else {
- if (idx && !idx.inv)
- idx.inv = new InventoryMeta();
- return idx;
- }
+ getItem : function getItem(item){
+ if ( typeof item == 'number' ) {
+ return this.slots[ (item < 0 ? this.max+item : item) ];
+ } else
+ return item;
},
/**
* @return {Boolean} Whether the item is in this container.
*/
hasItem : function hasItem(id){
- if (id instanceof Item) id = id.__id__;
+ if (id && id.isItem) id = id.__id__;
return !!this.items[id];
},
* @return {this}
*/
addItem : function addItem(item){
- if ( this.hasItem(item) )
+ if ( !item || this.hasItem(item) )
return this;
+ if ( this.size >= this.max )
+ throw new Error('Insufficient room to add item!');
+ else if ( !this.checkReqs(item) )
+ throw new Error('Item '+item+' not legal in container '+this+'!');
else
return this.moveItem(item, this._getEmptySlot());
},
* @return {Item} The removed item or null on failure.
*/
removeItem : function removeItem(item, options){
- options = options || {};
+ options = Y.isPlainObject(options) ? options : {};
item = this.getItem(item);
if (!item)
return item;
return item;
},
+ removeAllItems : function removeAllItems(){
+ Y(this.children).invoke('removeAllItems');
+
+
+ return this;
+ },
/**
* @protected
- * @return {Integer} Index of first empty slot in this container.
+ * @param {Item} [item] If present, we first test to ensure container can accept the item.
+ * @return {Integer} Index of first empty slot in this container, or -1 if
+ * container is full or if item does not meet requirements.
*/
- _getEmptySlot : function _getEmptySlot(){
- var slots = this.slots
- , max = this.max;
-
- if (this.size >= max)
+ _getEmptySlot : function _getEmptySlot(item){
+ if ( !this.canAddItem(item) )
return -1;
- for (var i=0, v=slots[i]; i<max; v=slots[++i])
+ for (var slots = this.slots, i=0, v=slots[i]; i<this.max; v=slots[++i])
if (v === undefined)
return i;
* @protected
* @param {Item} item
* @param {Number} [idx] Container position at which to insert item. If missing, first open slot will be used.
+ * @param {Boolean} [didDisplace=false] Whether this put displaced another item, implying the size is unchanged.
* @return {Boolean}
*/
_putItem : function _putItem(item, idx, didDisplace){
if ( 0 <= idx && idx < this.max )
return false;
- // Already in bag? Remove from current slot
if ( this.items[id] )
- delete this.slots[inv.idx];
+ delete this.slots[inv.idx]; // Already in bag? Remove from current slot
else if (!didDisplace)
this.size++; // Otherwise increment size
inv.idx = idx;
inv.bag = this;
+ if (item.isItemContainer)
+ this.children[id] = item;
+
return true;
},
* @param {Item} item
* @return {Boolean}
*/
- _removeItem : function removeItem(item, oldIdx){
+ _removeItem : function _removeItem(item, oldIdx){
if (!item) return false;
var id = item.__id__
;
delete this.items[id];
+ delete this.children[id];
if ( hasItem ) {
this.size--;
delete this.slots[idx];
if (!itemMoved)
inv.idx = inv.bag = null;
+ // TODO: When container is lost, notify unit of lost items,
+ // but do not remove them from the container.
+ // if (item.isItemContainer)
+ // item.removeAllItems();
return true;
}
-});
+})
+,
+
+ContainerItem =
+exports['ContainerItem'] =
+evt.subclass('ContainerItem', {
+ __mixins__ : [ Container ],
+
+ max : 1, // Container capacity
+ reqs : null, // Array of tag requirements to be stored here // XXX: 'not(bag)'
+ equipsContents : false, // Whether held items are equipped
+
+
+ init : function initContainer(owner, items){
+ this.setup(owner, items);
+ }
+
+})
+,
-function InventoryMeta(bag, idx, links){
+ContainerMeta =
+exports['ContainerMeta'] =
+function ContainerMeta(idx, bag, links){
this.bag = bag;
this.idx = idx;
this.links = links || [];
}
-Y.core.extend(InventoryMeta.prototype, {
+;
+
+Y.core.extend(ContainerMeta.prototype, {
clear : function clear(){
this.bag = this.idx = null;
this.links = [];
},
clone : function clone(){
- return new InventoryMeta(this.bag, this.idx, this.links); // Don't care about links
+ return new ContainerMeta(this.bag, this.idx, this.links); // Don't care about links
},
toString : function(){
- return "BagLoc(idx="+this.idx+", bag="+this.bag+")";
+ return "(idx="+this.idx+", bag="+this.bag+")";
}
-})
+});
+
--- /dev/null
+var Y = require('Y').Y
+, container = require('tanks/inventory/container')
+, inventory = require('tanks/inventory/inventory')
+, bag = require('tanks/inventory/bag')
+, bagbag = require('tanks/inventory/bagbag')
+, belt = require('tanks/inventory/belt')
+;
+Y.core.extend(exports, {
+ 'inventory' : inventory,
+ 'Inventory' : inventory.Inventory,
+
+ 'container' : container,
+ 'Container' : container.Container,
+
+ 'bag' : bag,
+ 'Bag' : bag.Bag,
+
+ 'bagbag' : bagbag,
+ 'BagBag' : bagbag.BagBag,
+
+ 'belt' : belt,
+ 'Belt' : belt.Belt
+});
+var Y = require('Y').Y
+, deepcopy = require('Y/types/object').deepcopy
+
+, evt = require('evt')
+
+, container = require('tanks/inventory/container')
+, Container = container.Container
+, ContainerItem = container.ContainerItem
+, ContainerMeta = container.ContainerMeta
+, Item = require('tanks/thing/item').Item
+, Bag = require('tanks/inventory/bag').Bag
+, BagBag = require('tanks/inventory/bagbag').BagBag
+,
+
+
+Inventory =
+exports['Inventory'] =
+evt.subclass('Inventory', {
+ __mixins__ : [], // Container
+ __bind__ : [],
+
+ /// Setup ///
+
+ defaultBag : 'backpack', // name of default bag for pickups
+ equipSlots : null, // name -> Container options; implies equipsContents=true
+ bagSlots : 1, // Number of bags that can be put on the root toolbar
+ beltSlots : 4, // Number of items that can be hotkeyed to your belt
+
+ /// Bookkeeping ///
+
+ owner : null, // {Unit}
+ belt : null, // {Belt} Size determined by beltSlots
+ bagbag : null, // {BagBag} Size determined by bagSlots
+
+ items : {}, // id -> {Item} All items in all bags
+ equipment : {}, // name -> {Item} All equipped items
+ bags : {}, // id -> {Container} All bags in all bags
+ namedBags : {}, // name -> {Container} All named bags in all bags
+ equipBags : {}, // name -> {EquipSlot} All EquipSlot containers
+
+ // Y-wrapped
+ _items : null,
+ _bags : null,
+
+
+
+ init : function initInventory(owner){
+ var S = this.settings = deepcopy(evt.aggregate(owner, 'inventory'));
+ Y.core.extend(this, S);
+ this.owner = owner;
+
+ this.items = {};
+ this.equipment = {};
+ this.bags = {};
+ this.namedBags = {};
+ this.equipBags = {};
+
+ this._items = Y(this.items);
+ this._bags = Y(this.bags);
+
+ this.bagbag = new BagBag(this, owner);
+
+ // instantiate:
+ // - backpack
+ // - equip slots
+ // - belt
+ },
+
+
+ /// Item Management ///
+
+ /**
+ * Tests whether inventory has room for this item (in a container that would accept it).
+ * @param {Item} item
+ * @return {Boolean}
+ */
+ /**
+ * Tests whether specified container would accept the item.
+ * @param {Item} item
+ * @param {Container} bag Container to which to add.
+ * @return {Boolean}
+ */
+ canAddItem : function canAddItem(item, bag){
+ if (bag === undefined)
+ return this._bags.invoke('canAddItem', item).any();
+ bag = this.getBag(bag);
+ return bag && bag.canAddItem(item);
+ },
+
+ /**
+ * Adds item to the first empty bag (starting with the default bag) that will accept it.
+ * @param {Item} item The item.
+ * @return {this}
+ */
+ /**
+ * Adds item to the selected bag.
+ * @param {Item} item The item.
+ * @param {Container|String|Number} bag Container to add to; a String is acceptable if the Container is named.
+ * @return {this}
+ */
+ addItem : function addItem(item, bag){
+ bag = this.getBag(bag, true);
+ if ( !bag )
+ throw new Error('Cannot add item: unknown bag '+bag+'!');
+ return bag.addItem(item);
+ },
+
+ /**
+ * @param {Item} item
+ * @param {String|Number|Container} bag Destination container.
+ * @param {Number} [idx] Destination index. If omitted, first empty index will be used.
+ * @return {this}
+ */
+ moveItem : function moveItem(item, bag, idx){
+ if (!item)
+ throw new Error('Cannot move item: no item supplied!');
+
+ bag = this.getBag(bag);
+ if ( !bag )
+ throw new Error('Cannot move item: unknown bag '+bag+'!');
+ if ( !bag.checkReqs(item) )
+ throw new Error('Cannot move item '+item+' to container '+bag+'!');
+
+ idx = (idx !== undefined ? idx : bag._getEmptySlot());
+ return bag.moveItem(item, idx);
+ },
+
+ /**
+ * Removes item from inventory.
+ * @param {Item|Number} item Item to remove (or index of that item in the bag).
+ * @param {Bag|Number|String} [bag] Bag in which to look up the index. If omitted, all bags will be searched.
+ * @return {Item} The removed item or null on failure.
+ */
+ removeItem : function removeItem(item, bag, options){
+ if ( !this.hasItem(item, bag) )
+ return null;
+ item = this.getItem(item, bag);
+ if ( !item )
+ return null;
+
+ return item.inv.bag.removeItem(item, options);
+ // TODO: Fire event?
+ },
+
+ /**
+ * @param {Item|Number} item Item or Id
+ * @param {String|Number|Container} [bag] Container to search. If omitted, all bags will be checked.
+ * @return {Boolean}
+ */
+ hasItem : function hasItem(item, bag){
+ if ( item && item.isItem )
+ item = item.__id__;
+
+ if ( bag === undefined )
+ return !!this.items[item];
+
+ bag = this.getBag(bag);
+ return bag && bag.hasItem(item);
+ },
+
+ /**
+ * @param {Number} idx Index into the default bag from which to retrieve the item.
+ * @return {Item} The item at that index.
+ */
+ /**
+ * @param {Number} idx Index in queried container.
+ * @param {Container} bag Container to query for an item.
+ * @return {Item} The item at that index.
+ */
+ getItem : function getItem(idx, bag){
+ if ( typeof idx == 'number' ) {
+ bag = (bag === undefined ? this.defaultBag : this.getBag(bag));
+ return bag ? bag.getItem(idx) : undefined;
+ } else
+ return idx;
+ },
+
+ /**
+ * Searches for an available (bag,slot), optionally restricted by an item.
+ * @param {Item} [item] If included, only bags which will accept the item are considered.
+ * @param {Container|String|Number} [bag] Container to search. If omitted, all containers
+ * will be searched, starting with the default bag.
+ * @return {ContainerMeta} The (bag,idx) information about the empty slot, or null if no matching slot was found.
+ */
+ findEmptySlot : function findEmptySlot(item, bag){
+ var checkSpecificBag = (bag !== undefined)
+ , idx = -1
+ ;
+ bag = this.getBag(bag);
+ item = this.getItem(item, bag);
+
+ if ( checkSpecificBag ) {
+ idx = bag._getEmptySlot(item);
+ } else {
+ idx = this.defaultBag._getEmptySlot(item);
+ if (idx === -1)
+ for (var id in this.bags) {
+ bag = this.bags[id];
+ if ( bag.canAddItem(item) ) {
+ idx = bag._getEmptySlot();
+ break;
+ }
+ }
+ }
+
+ return (idx === -1) ? null : new ContainerMeta(bag, idx);
+ },
+
+
+
+ /// Bag Management ///
+
+ canAddBag : function canAddBag(bag){
+ return this.bagbag.canAddItem(bag);
+ },
+
+ checkReqs : function checkReqs(bag){
+ return this.bagbag.checkReqs(bag);
+ },
+
+ /**
+ * @param {String|Number|Container} query Queries for a bag:
+ * - String: Looks up bag by name
+ * - Number: Looks up bag by index in BagBag
+ * - Container: Passed through
+ * @param {Boolean} [orDefault=false] If true, an unspecified query will return the default bag.
+ * @return {Container} The queried bag, or the default bag if `orDefault`, otherwise undefined.
+ */
+ getBag : function getBag(query, orDefault){
+ if (query === undefined && orDefault)
+ return this.defaultBag;
+ return ( (typeof query == 'string') ? this.namedBags[query] : this.bagbag.getItem(query) );
+ // return ( (typeof idx == 'string') ? this.namedBags[idx] : this.bagbag.getItem(idx)
+ // || (orDefault ? this.defaultBag : undefined) );
+ },
+
+ /**
+ * @param {String} name
+ * @return {Container}
+ */
+ getBagByName : function getBagByName(name){
+ return this.namedBags[name];
+ },
+
+ hasBag : function hasBag(id){
+ return this.bagbag.hasItem(id);
+ },
+
+ addBag : function addBag(bag){
+ this.bagbag.addItem(bag);
+ return this;
+ },
+
+ moveBag : function moveBag(bag, idx){
+ this.bagbag.moveItem(bag, idx);
+ return this;
+ },
+
+ removeBag : function removeBag(bag, options){
+ this.bagbag.removeItem(bag, options);
+ return this;
+ },
+
+ removeAllBags : function removeAllBags(){
+ this.bagbag.removeAllItems();
+ return this;
+ }
+
+})
+;
--- /dev/null
+var Y = require('Y')
+, evt = require('evt')
+,
+
+Indexer =
+exports['Indexer'] =
+evt.subclass('Indexer', {
+ __mixins__ : [],
+ __bind__ : [ 'addWord', 'addWords', 'addField', 'addObject', 'matchQuery', '_reduceMatches' ],
+ __static__ : {},
+
+ className : 'Indexer',
+ _delimiter : ' ',
+ _kvsep : ':',
+ _words : null,
+ _fields : null,
+
+
+
+ init : function initIndexer(this, delimiter, kvsep){
+ this._delimiter = delimiter || ' ';
+ this._kvsep = kvsep || ':';
+ this._words = {};
+ this._fields = {};
+ },
+
+
+ addWord: function(object, word){
+ if ( !word ) return this;
+
+ var w = ''+word,
+ ws = this._words;
+ if ( ws[w] )
+ !ws[w].has(object) && ws[w].push(object);
+ else
+ ws[w] = [ object ];
+
+ return this;
+ },
+
+ addWords: function(object, words){
+ if ( !(words && object) ) return this;
+
+ this._normalizeWords(words)
+ .each(this.addWord.bind(this, object));
+
+ return this;
+ },
+
+ addField: function(object, field, words){
+ if ( !(field && object) ) return this;
+
+ words = words || object[field];
+ if ( !words ) return;
+
+ this.addWords(object, words);
+
+ var f = this._fields[field];
+ if ( !f )
+ f = this._fields[field] = new com.clearspring.Indexer( this._delimiter, this._kvsep );
+
+ f.addWords(object, words);
+
+ return this;
+ },
+
+ addObject: function(object, exclude){
+ if ( !exclude )
+ exclude = [ Function ];
+ else if ( !(exclude instanceof Array) )
+ exclude = [ exclude ];
+
+ basis.each(object, function(words, field){
+ if ( !exclude.has(field, basis.type(words)) )
+ this.addField(object, field);
+ });
+ return this;
+ },
+
+ _normalizeWords: function(words){
+ if ( !words )
+ return [];
+
+ if (words instanceof Array)
+ words = words.join(this._delimiter);
+ else
+ words += '';
+
+ return words
+ // case insensitive
+ .toLowerCase()
+
+ // normalize whitespace (counting non-WS delimiters)
+ .replace(/([\s-.:]+)/g, this._delimiter)
+
+ // strip non-word characters
+ .replace(/([^ a-z0-9_-]|^\s+|\s+$)/g, '')
+
+ // return words
+ .split(this._delimiter);
+ },
+
+ _reduceMatches: function(results, word){
+ if ( word in this._words )
+ return results.concat(this._words[word]);
+ else
+ return results;
+ },
+
+ _match: function(field, words){
+ if ( field && !this._fields[field] )
+ return [];
+
+ var matcher = ( !field ?
+ this._reduceMatches : this._fields[field]._reduceMatches );
+
+ return this._normalizeWords(words)
+ .reduce(matcher, [])
+ .unique();
+ },
+
+ _processAST: function(node){
+ if (!node)
+ return [];
+
+ if (node instanceof QueryParser.And)
+ return this._processAST(node.lhs)
+ .intersect(this._processAST(node.rhs));
+
+ else if (node instanceof QueryParser.Or)
+ return this._processAST(node.lhs)
+ .union(this._processAST(node.rhs));
+
+ else if (node instanceof QueryParser.Not)
+ return Object.values(this._words)
+ .difference(this._processAST(node.expr));
+
+ else if (node instanceof QueryParser.Field)
+ return this._match(node.field, node.word);
+
+ else if (node instanceof Array)
+ return node;
+
+ else
+ return this._match(null, node);
+ },
+
+ matchQuery: function(query){
+ return this._processAST(QueryParser(query));
+ }
+
+})
+;
+
--- /dev/null
+//#ensure "jsparse"
+
+/* Item Requirements Grammar
+
+word := [a-zA-Z0-9_-]+
+field := word ':' word
+group := '(' query ')'
+term := group | field | word
+
+and := term 'and' term | term '&&' term | term term
+or := term 'or' term | term '||' term
+not := 'not' term | '!' term
+op := and | or | not
+
+query := op | term
+
+*/
+var Query = function(state) { return Query(state); };
+
+function toNameString(){
+ return !this.constructor ? "" : this.constructor.name;
+}
+
+function Symbol(p, Cls){
+ Cls.prototype.toString = toNameString;
+ return action(p, function(lr) {
+ return new Cls(lr[0], lr[1]);
+ });
+}
+
+function Field(f, w){
+ this.field = f;
+ this.word = w;
+}
+
+function And(lhs, rhs) {
+ this.lhs = lhs;
+ this.rhs = rhs;
+}
+
+function Or(lhs, rhs) {
+ this.lhs = lhs;
+ this.rhs = rhs;
+}
+
+function Not(expr) {
+ this.expr = expr;
+}
+
+var word = join_action(
+ repeat1(choice(
+ range('a', 'z'),
+ range('A', 'Z'),
+ range('0', '9'),
+ '_', '-', '$' )),
+ '');
+// var field = Symbol(sequence(word, expect(':'), word), Field);
+var group = action(wsequence(expect('('), Query, expect(')')), function(g){ return g[0]; });
+
+// var term = choice(group, field, word);
+var term = choice(group, word);
+
+var and = Symbol(choice(
+ wsequence(term, expect(token('and')), Query),
+ wsequence(term, expect(token('&&')), Query),
+ wsequence(term, Query)), And);
+
+var or = Symbol(choice(
+ wsequence(term, expect(token('or')), Query),
+ wsequence(term, expect(token('||')), Query)), Or);
+
+var not = Symbol(choice(
+ wsequence(expect(token('not')), Query),
+ wsequence(expect('!'), Query)), Not);
+
+var op = choice(not, or, and);
+
+var Query = choice(op, term);
+
+function QueryParser(s){
+ return (Query(ps(s)) || {}).ast;
+}
+
+QueryParser.And = And;
+QueryParser.Or = Or;
+QueryParser.Not = Not;
+QueryParser.Field = Field;
+exports['QueryParser'] = QueryParser;
+
+
+var Y = require('Y')
+, evt = require('evt')
+,
+
+Requirements =
+exports['Requirements'] =
+evt.subclass('Requirements', {
+ __mixins__ : [],
+ __bind__ : [],
+
+ ast : null,
+
+
+ init : function initRequirements(reqs){
+ this.reqs = reqs;
+ this.ast = QueryParser(reqs);
+ },
+
+ test : function test(tags){
+ return !(this.reqs && this.reqs.length) ||
+ (tags && tags.length && this._test(tags, this.ast));
+ },
+
+ _test: function _test(tags, node){
+ if (!node)
+ return true;
+
+ if (node instanceof QueryParser.And)
+ return this._test(tags, node.lhs) && this._test(tags, node.rhs);
+
+ else if (node instanceof QueryParser.Or)
+ return this._test(tags, node.lhs) || this._test(tags, node.rhs);
+
+ else if (node instanceof QueryParser.Not)
+ return !this._test(tags, node.expr);
+
+ else if (node instanceof Array) {
+ console.log('wtf! array node:', node, 'tags:', tags);
+ // throw new Error('wtf! array node! ['+node+']');
+ // return node.every(this._test.bind(this, tags));
+ return node.every(tags.has, tags);
+
+ } else
+ return tags.has(node);
+ }
+
+
+})
+;
+
+
, Item = require('tanks/thing/item').Item
, kNull = op.K(null)
-
,
+
Inventoried =
exports['Inventoried'] =
Mixin.subclass('Inventoried', {
, BoundsType = require('tanks/constants').BoundsType
, Buff = require('tanks/effects/buff').Buff
, Thing = require('tanks/thing/thing').Thing
+, ContainerMeta = require('tanks/inventory/container').ContainerMeta
, ITEM_SIZE = REF_SIZE * 0.5
, kNull = op.K(null)
Thing.subclass('Item', {
// __mixins__ : [ ],
__bind__ : [ 'onCollide', 'onAcquired', 'onLost', 'onEquip', 'onUnequip', 'onBuffDeath' ],
+ isItem : true,
align : 0, // 0 reserved for neutral units
stats : { hp:Infinity, move:0, power:0, speed:0 },
dealDamage : op.nop,
- // inactive
- active : false,
-
+ active : false, // inactive in the event loop
/// Instance Properties ///
name : 'Dummy', // {String} Display name
owner : null, // {Unit} Owner when picked up.
currentBuffs : null, // {Buff...} Buffs applied to owner
- isEquipped : false,
-
- // Inventory Bookkeeping
- // inv : {
- // links : [], // {Array} Pointers to Belts that link to this item
- // idx : null, // {Integer} Index in bag
- // bag : null // {Container} Containing bag
- // },
+ inv : null, // {ContainerMeta} Inventory Bookkeeping
+ isEquipped : false, // XXX: ?
init : function initItem(){
this.currentActive = new Y.YArray();
this.currentPassive = new Y.YArray();
+ this.inv = new ContainerMeta();
this.activateGauge = new CooldownGauge(this.cooldowns.activate, REF_SIZE+1,REF_SIZE+1);
this.on('collide', this.onCollide);
this.on('item.unequip', this.onUnequip);
},
- tick : function tick(elapsed, now, ticks){
- // Buffs will take care of themselves as part of the unit
- },
-
- addToInventory : function addToInventory(unit){
- if (!unit.hasInventory)
- throw new Error('Unit has no inventory! '+unit);
- unit.addItem(this);
- return this;
- },
-
- removeFromInventory : function removeFromInventory(){
- if (this.owner)
- this.owner.removeItem(this);
- return this;
- },
-
activate : function activate(){
- console.log(this+' activated!');
+ // console.log(this+' activated!');
if ( this.owner && this.cooldowns.activate.activate() ) {
var buffs = this.currentBuffs
return this;
},
+ /// Event Handlers ///
+
onBuffDeath : function onBuffDeath(evt, buff){
this.currentBuffs.remove(buff);
this.currentActive.remove(buff);
},
onLost : function onLost(evt){
- var current = this.currentBuffs
+ var lostItem = evt.data.item
+ , current = this.currentBuffs
, unit = this.owner
;
+ // Bags are Items AND Containers, and are thus sent notifications when
+ // their contents change.
+ if (lostItem !== this)
+ return;
+
if ( current.length )
current.invoke('die', 'item.lost');
, config = require('tanks/config').config
, Tank = require('tanks/thing/tank').Tank
+, Inventory = require('tanks/inventory/inventory').Inventory
, Inventoried = require('tanks/mixins/inventoried').Inventoried
,
Player =
exports['Player'] =
Tank.subclass('Player', {
- __mixins__ : [ Inventoried ],
+ // __mixins__ : [ Inventoried ],
+ __bind__ : [ 'keydown', 'keyup', 'mousedown', 'mouseup', 'mousemove' ],
enableGameLogging : false,
this.gameLog = [];
this.queue = [];
this.activeKeys = new Y.YArray();
+ this.inventory = new Inventory(this);
- this.keydown = this.keydown.bind(this);
- this.keyup = this.keyup.bind(this);
- this.mousedown = this.mousedown.bind(this);
- this.mouseup = this.mouseup.bind(this);
- this.mousemove = this.mousemove.bind(this);
-
- this.replayMode = !!replay;
+ // this.keydown = this.keydown.bind(this);
+ // this.keyup = this.keyup.bind(this);
+ // this.mousedown = this.mousedown.bind(this);
+ // this.mouseup = this.mouseup.bind(this);
+ // this.mousemove = this.mousemove.bind(this);
if (this.replayMode) {
this.replay = replay
,
-ItemContainerUI =
-exports['ItemContainerUI'] =
-HtmlLayer.subclass('ItemContainerUI', {
+ContainerUI =
+exports['ContainerUI'] =
+HtmlLayer.subclass('ContainerUI', {
+ __bind__ : [ 'onItemUpdated', 'onDrop' ],
_layerClasses : 'item-container hud',
name : null,
title : null,
- showTitle : true,
+ showTitle : false,
defaultContainer : false,
max : 1,
- init : function initItemContainerUI(name, unit, items, options){
- Y.bindAll(this, 'onItemUpdated', 'onDrop');
+ init : function initContainerUI(unit, container, options){
if (options) Y.core.extend(this, options);
- this.name = name;
this.unit = unit;
- this.items = items;
+ this.container = container;
- this._layerId = this.name+'_slot';
+ if (!this._layerId && this.name) this._layerId = this.name+'_slot';
HtmlLayer.init.call(this);
if (this.showTitle)
},
_makeSlot : function _makeSlot(idx){
- return new ItemContainerUISlot(this, idx).appendTo(this);
+ return new ContainerUISlot(this, idx).appendTo(this);
},
refresh : function refresh(){
,
-ItemContainerUISlot =
-exports['ItemContainerUISlot'] =
-Layer.subclass('ItemContainerUISlot', {
+ContainerUISlot =
+exports['ContainerUISlot'] =
+Layer.subclass('ContainerUISlot', {
_layerClasses : 'item-container slot hud ',
hasCanvas : false,
item : null,
- init : function initItemContainerUISlot(backpack, idx){
+ init : function initContainerUISlot(container, idx){
Y.bindAll(this, 'onActivate', 'onDragStart');
Layer.init.call(this);
- this.backpack = backpack;
- this.inventory = backpack.inventory;
+ this.container = container;
+ this.inventory = container.inventory;
this.idx = idx;
this.layer.addClass('slot'+idx);
this.inner =
refresh : function refresh(){
this.layer.data({
- 'backpack-slot': this,
+ 'container-slot': this,
'idx': this.idx
});
- var item = this.inventory.backpack[this.idx];
+ var item = this.container.slots[this.idx];
if (item)
this.setItem(item);
else