Shapes!
authordsc <david.schoonover@gmail.com>
Sun, 31 Oct 2010 06:31:11 +0000 (23:31 -0700)
committerdsc <david.schoonover@gmail.com>
Sun, 31 Oct 2010 06:31:11 +0000 (23:31 -0700)
css/lttl.css
src/Y/to-function.js [new file with mode: 0644]
src/Y/y-collection.js
src/portal/layer.js
src/portal/shape.js
src/tanks/lttl.js

index d746829..2a24362 100644 (file)
@@ -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 (file)
index 0000000..936376b
--- /dev/null
@@ -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;
+     });
index ee317ee..0c9fab1 100644 (file)
@@ -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){
index 83613c2..a59db7a 100644 (file)
@@ -45,10 +45,9 @@ Layer = new Y.Class('Layer', {
         
         this.layer = jQuery('<div/>')
             .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()+' )';
     }
 });
 
index a7b4c95..0e225b7 100644 (file)
@@ -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]);
     }
-    
 });
 
index fced4b3..c011cd5 100644 (file)
@@ -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()
 ;