From: dsc Date: Sun, 31 Oct 2010 06:31:11 +0000 (-0700) Subject: Shapes! X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=d37f8b85a0f2f25ba1719454bc886a6512014937;p=tanks.git Shapes! --- diff --git a/css/lttl.css b/css/lttl.css index d746829..2a24362 100644 --- a/css/lttl.css +++ b/css/lttl.css @@ -6,7 +6,8 @@ ul, ol, li { list-style: none ! important; margin:0; padding:0; } .rounded { border-radius:1em; -moz-border-radius:1em; -webkit-border-radius:1em; } -#viewport { position:relative; top:1em; width:500px; height:500px; margin:0 auto; } +#viewport { position:relative; top:1em; width:500px; height:500px; margin:0 auto; + outline:1px solid #ccc; } #howto { position:fixed; top:3em; right:1em; color:#BFBFBF; } diff --git a/src/Y/to-function.js b/src/Y/to-function.js new file mode 100644 index 0000000..936376b --- /dev/null +++ b/src/Y/to-function.js @@ -0,0 +1,221 @@ +/* + * Author: Oliver Steele + * Copyright: Copyright 2007 by Oliver Steele. All rights reserved. + * License: MIT License + * Homepage: http://osteele.com/javascripts/functional + * Created: 2007-07-11 + * Version: 1.0.2 + * + * + * This defines "string lambdas", that allow strings such as `x+1` and + * `x -> x+1` to be used in some contexts as functions. + */ + + +/// ^ String lambdas + +/** + * Turns a string that contains a JavaScript expression into a + * `Function` that returns the value of that expression. + * + * If the string contains a `->`, this separates the parameters from the body: + * >> 'x -> x + 1'.lambda()(1) -> 2 + * >> 'x y -> x + 2*y'.lambda()(1, 2) -> 5 + * >> 'x, y -> x + 2*y'.lambda()(1, 2) -> 5 + * + * Otherwise, if the string contains a `_`, this is the parameter: + * >> '_ + 1'.lambda()(1) -> 2 + * + * Otherwise if the string begins or ends with an operator or relation, + * prepend or append a parameter. (The documentation refers to this type + * of string as a "section".) + * >> '/2'.lambda()(4) -> 2 + * >> '2/'.lambda()(4) -> 0.5 + * >> '/'.lambda()(2,4) -> 0.5 + * Sections can end, but not begin with, `-`. (This is to avoid interpreting + * e.g. `-2*x` as a section). On the other hand, a string that either begins + * or ends with `/` is a section, so an expression that begins or ends with a + * regular expression literal needs an explicit parameter. + * + * Otherwise, each variable name is an implicit parameter: + * >> 'x + 1'.lambda()(1) -> 2 + * >> 'x + 2*y'.lambda()(1, 2) -> 5 + * >> 'y + 2*x'.lambda()(1, 2) -> 5 + * + * Implicit parameter detection ignores strings literals, variable names that + * start with capitals, and identifiers that precede `:` or follow `.`: + * >> map('"im"+root', ["probable", "possible"]) -> ["improbable", "impossible"] + * >> 'Math.cos(angle)'.lambda()(Math.PI) -> -1 + * >> 'point.x'.lambda()({x:1, y:2}) -> 1 + * >> '({x:1, y:2})[key]'.lambda()('x') -> 1 + * + * Implicit parameter detection mistakenly looks inside regular expression + * literals for variable names. It also doesn't know to ignore JavaScript + * keywords and bound variables. (The only way you can get these last two is + * with a function literal inside the string. This is outside the intended use + * case for string lambdas.) + * + * Use `_` (to define a unary function) or `->`, if the string contains anything + * that looks like a free variable but shouldn't be used as a parameter, or + * to specify parameters that are ordered differently from their first + * occurrence in the string. + * + * Chain `->`s to create a function in uncurried form: + * >> 'x -> y -> x + 2*y'.lambda()(1)(2) -> 5 + * >> 'x -> y -> z -> x + 2*y+3*z'.lambda()(1)(2)(3) -> 14 + * + * `this` and `arguments` are special: + * >> 'this'.call(1) -> 1 + * >> '[].slice.call(arguments, 0)'.call(null,1,2) -> [1, 2] + */ +String.prototype.lambda = function() { + var params = [], + expr = this, + sections = expr.ECMAsplit(/\s*->\s*/m); + if (sections.length > 1) { + while (sections.length) { + expr = sections.pop(); + params = sections.pop().split(/\s*,\s*|\s+/m); + sections.length && sections.push('(function('+params+'){return ('+expr+')})'); + } + } else if (expr.match(/\b_\b/)) { + params = '_'; + } else { + // test whether an operator appears on the left (or right), respectively + var leftSection = expr.match(/^\s*(?:[+*\/%&|\^\.=<>]|!=)/m), + rightSection = expr.match(/[+\-*\/%&|\^\.=<>!]\s*$/m); + if (leftSection || rightSection) { + if (leftSection) { + params.push('$1'); + expr = '$1' + expr; + } + if (rightSection) { + params.push('$2'); + expr = expr + '$2'; + } + } else { + // `replace` removes symbols that are capitalized, follow '.', + // precede ':', are 'this' or 'arguments'; and also the insides of + // strings (by a crude test). `match` extracts the remaining + // symbols. + var vars = this.replace(/(?:\b[A-Z]|\.[a-zA-Z_$])[a-zA-Z_$\d]*|[a-zA-Z_$][a-zA-Z_$\d]*\s*:|this|arguments|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, '').match(/([a-z_$][a-z_$\d]*)/gi) || []; // ' + for (var i = 0, v; v = vars[i++]; ) + params.indexOf(v) >= 0 || params.push(v); + } + } + return new Function(params, 'return (' + expr + ')'); + // return eval('(function('+params+'){return ('+expr+'); })'); +} + +/// Turn on caching for `string` -> `Function` conversion. +String.prototype.lambda.cache = function() { + var proto = String.prototype, + cache = {}, + uncached = proto.lambda, + cached = function() { + var key = '#' + this; // avoid hidden properties on Object.prototype + return cache[key] || (cache[key] = uncached.call(this)); + }; + cached.cached = function(){}; + cached.uncache = function(){proto.lambda = uncached}; + proto.lambda = cached; +} + +/** + * ^^ Duck-Typing + * + * Strings support `call` and `apply`. This duck-types them as + * functions, to some callers. + */ + +/** + * Coerce the string to a function and then apply it. + * >> 'x+1'.apply(null, [2]) -> 3 + * >> '/'.apply(null, [2, 4]) -> 0.5 + */ +String.prototype.apply = function(thisArg, args) { + return this.toFunction().apply(thisArg, args); +} + +/** + * Coerce the string to a function and then call it. + * >> 'x+1'.call(null, 2) -> 3 + * >> '/'.call(null, 2, 4) -> 0.5 + */ +String.prototype.call = function() { + return this.toFunction().apply(arguments[0], + Array.prototype.slice.call(arguments, 1)); +} + +/// ^^ Coercion + +/** + * Returns a `Function` that perfoms the action described by this + * string. If the string contains a `return`, applies + * `new Function` to it. Otherwise, this function returns + * the result of `this.lambda()`. + * >> '+1'.toFunction()(2) -> 3 + * >> 'return 1'.toFunction()(1) -> 1 + */ +String.prototype.toFunction = function() { + var body = this; + if (body.match(/\breturn\b/)) + return new Function(this); + return this.lambda(); +} + +/** + * Returns this function. `Function.toFunction` calls this. + * >> '+1'.lambda().toFunction()(2) -> 3 + */ +Function.prototype.toFunction = function() { + return this; +} + +/** + * Coerces `fn` into a function if it is not already one, + * by calling its `toFunction` method. + * >> Function.toFunction(function() {return 1})() -> 1 + * >> Function.toFunction('+1')(2) -> 3 + * + * `Function.toFunction` requires an argument that can be + * coerced to a function. A nullary version can be + * constructed via `guard`: + * >> Function.toFunction.guard()('1+') -> function() + * >> Function.toFunction.guard()(null) -> null + * + * `Function.toFunction` doesn't coerce arbitrary values to functions. + * It might seem convenient to treat + * `Function.toFunction(value)` as though it were the + * constant function that returned `value`, but it's rarely + * useful and it hides errors. Use `Functional.K(value)` instead, + * or a lambda string when the value is a compile-time literal: + * >> Functional.K('a string')() -> "a string" + * >> Function.toFunction('"a string"')() -> "a string" + */ +Function.toFunction = function(value) { + return value.toFunction(); +} + +// Utilities + +// IE6 split is not ECMAScript-compliant. This breaks '->1'.lambda(). +// ECMAsplit is an ECMAScript-compliant `split`, although only for +// one argument. +String.prototype.ECMAsplit = + // The test is from the ECMAScript reference. + ('ab'.split(/a*/).length > 1 + ? String.prototype.split + : function(separator, limit) { + if (typeof limit != 'undefined') + throw "ECMAsplit: limit is unimplemented"; + var result = this.split.apply(this, arguments), + re = RegExp(separator), + savedIndex = re.lastIndex, + match = re.exec(this); + if (match && match.index == 0) + result.unshift(''); + // in case `separator` was already a RegExp: + re.lastIndex = savedIndex; + return result; + }); diff --git a/src/Y/y-collection.js b/src/Y/y-collection.js index ee317ee..0c9fab1 100644 --- a/src/Y/y-collection.js +++ b/src/Y/y-collection.js @@ -102,9 +102,8 @@ YBase.subclass('YCollection', { 'zip' : function(){ var sequences = new Y(arguments).map( Y.limit(1) ).unshift(this); return this.map(function(_, k){ - var r = sequences.map( Y.op.get(k) ); - return r; - }); + return sequences.invoke('attr', k); + }).invoke('end'); }, 'pluck' : function(key){ diff --git a/src/portal/layer.js b/src/portal/layer.js index 83613c2..a59db7a 100644 --- a/src/portal/layer.js +++ b/src/portal/layer.js @@ -45,10 +45,9 @@ Layer = new Y.Class('Layer', { this.layer = jQuery('
') .addClass(this._cssClasses) - .attr('layer', this) .append(this.canvas); - // this.layer[0].layer = this; + this.layer[0].layer = this; }, /// Scene Graph Heirarchy /// @@ -144,7 +143,7 @@ Layer = new Y.Class('Layer', { * Sets the position of this node, and then returns it. * @param {Object} pos An object with "top" and/or "left" properties as in `position(top, left)`. */ - position : function(top, left){ + position : function(left, top){ if (top === undefined && left === undefined) return this.layer.position(); @@ -161,32 +160,6 @@ Layer = new Y.Class('Layer', { css : makeDelegate('css'), - // position : function(pos){ - // if (pos) { - // this.x = (pos.x || pos.left); - // this.y = (pos.y || pos.top ); - // return this._updatePos(); - // } else { - // var x = this.x, y = this.y; - // return { x: x, left: x - // , y: y, top : y }; - // } - // }, - // - // _updatePos : function(ppos){ - // ppos = ppos || this.parent.position(); - // this.dirty = true; - // this.canvas.css({ - // 'left' : ppos.x + this.x , - // 'top' : ppos.y + this.y - // }); - // - // this.children.invoke('_updatePos', this.position()); - // - // return this; - // }, - - /// Drawing Functions /// @@ -264,7 +237,8 @@ Layer = new Y.Class('Layer', { /// Misc /// toString : function(){ - return this.className+'['+this.x+','+this.y+']( children='+this.children.size()+' )'; + var pos = (this.layer ? this.position() : {top:NaN, left:NaN}); + return this.className+'['+pos.left+','+pos.top+']( children='+this.children.size()+' )'; } }); diff --git a/src/portal/shape.js b/src/portal/shape.js index a7b4c95..0e225b7 100644 --- a/src/portal/shape.js +++ b/src/portal/shape.js @@ -8,6 +8,12 @@ CONTEXT_ATTRS = Y([ Shape = new Y.Class('Shape', Layer, { _cssClasses : 'portal layer shape', + x0: 0, y0: 0, + + _offsetX : 0, // We need our shape to be in the first quadrant + _offsetY : 0, // so we'll have to offset any negative shapes at the position call + + attr : function(key, value, def){ if (!key) return this; @@ -28,8 +34,38 @@ Shape = new Y.Class('Shape', Layer, { } else return Y.attr(this, key, value, def); + }, + + _calcOffset : function(which, values){ + values.unshift(0); + var self = this + , off = -1 * Math.min.apply(Math, values); + + self['_offset'+which.toUpperCase()] = off; + self[which+'s'] = values = values.map(function(v, i){ + // v += (i === 0 ? 0 : off); + return (self[which+i] = v + off); + }); + return values; + }, + + position : function(left, top){ + if (top === undefined && left === undefined) + return this.layer.position(); + + if (top && Y.isPlainObject(top)) + var pos = top; + else + var pos = { 'top':top, 'left':left }; + + if (pos.top !== undefined) pos.top -= this._offsetY; + if (pos.left !== undefined) pos.left -= this._offsetX; + + this.css(pos); + return this; } + }); Rect = new Y.Class('Rect', Shape, { @@ -58,9 +94,6 @@ Rect = new Y.Class('Rect', Shape, { }); Polygon = new Y.Class('Polygon', Shape, { - x0: 0, y0:0, - _offsetX : 0, // We need our triangle to be in the first quadrant - _offsetY : 0, // so we'll have to offset any negative shapes at the position call /** * Expects two arrays of coordinate-halfs, which could be zipped @@ -79,60 +112,24 @@ Polygon = new Y.Class('Polygon', Shape, { this.height( Math.max.apply(Math, ys) ); }, - _calcOffset : function(which, values){ - values.unshift(0); - var self = this - , off = -1 * Math.min.apply(Math, values); - - self['_offset'+which.toUpperCase()] = off; - self[which+'s'] = values = values.map(function(v, i){ - v += (i === 0 ? 0 : off); - self[which+i] = v - return v; - }); - return values; - }, - - position : function(left, top){ - if (top === undefined && left === undefined) - return this.layer.position(); - - if (top && Y.isPlainObject(top)) - var pos = top; - else - var pos = { 'top':top, 'left':left }; - - if (pos.top !== undefined) pos.top -= this.offsetX; - if (pos.left !== undefined) pos.left -= this.offsetY; - - this.css(pos); - return this; - }, - drawShape : function(ctx){ - var self = this; - + this.points.forEach(function(pair, i){ + ctx.lineTo.apply(ctx, pair); + }); + ctx.fill(); + return this; } }); Triangle = new Y.Class('Triangle', Polygon, { - x1: 0, y1: 0, - x2: 0, y2: 0, - - init : function (x1,y1, x2,y2){ + init : function(x1,y1, x2,y2){ Polygon.init.call(this, [x1,x2], [y1,y2]); - }, - - drawShape : function(ctx){ - } }); -Ellipse = new Y.Class('Ellipse', Shape, { - - drawShape : function(ctx){ - return this; +Quad = new Y.Class('Quad', Polygon, { + init : function(x1,y1, x2,y2, x3,y3){ + Polygon.init.call(this, [x1,x2,x3], [y1,y2,y3]); } - }); diff --git a/src/tanks/lttl.js b/src/tanks/lttl.js index fced4b3..c011cd5 100644 --- a/src/tanks/lttl.js +++ b/src/tanks/lttl.js @@ -13,10 +13,17 @@ L = new Layer(v) }) ).append( R = new Rect(250,250) - .position(50,150) + .position(150,50) .attr({ fillStyle : '#E2EEF5' }) + ).append( + T = new Triangle(25,-25, 50,0) + .position(150,300) + .css('z-index', 2) + .attr({ + fillStyle : '#E73075' + }) ).appendTo(v) .draw() ;