Improves shape rendering options.
authordsc <david.schoonover@gmail.com>
Wed, 10 Nov 2010 04:49:08 +0000 (20:49 -0800)
committerdsc <david.schoonover@gmail.com>
Wed, 10 Nov 2010 04:49:08 +0000 (20:49 -0800)
src/portal/layer.js
src/portal/math/line.js
src/portal/math/vec.js
src/portal/shape.js
test/math/index.php
test/math/math.test.js

index b3a2f7f..6772ea6 100644 (file)
@@ -31,7 +31,7 @@ Layer = new Y.Class('Layer', {
     
     // Transforms
     transform : null, // Object
-    
+    useCanvasScaling : false, // default to CSS3 scaling
     
     
     /// Setup ///
@@ -46,10 +46,10 @@ Layer = new Y.Class('Layer', {
         this.boundingBox = new Loc.Rect(0,0, 0,0);
         
         this.transform = {
-            origin    : new Loc('50%','50%'),
+            origin    : new Loc('50%','50%'), // rotational origin
             rotate    : 0,
             scale     : new Loc(1.0,1.0),
-            translate : new Loc(0,0)
+            translate : new Loc(0,0) // translates canvas
         };
         
         this.canvas = jQuery('<canvas />');
@@ -157,16 +157,15 @@ Layer = new Y.Class('Layer', {
         if (w === undefined)
             return this.layerWidth;
         
-        this.layerWidth = w;
-        this.layer.width(w);
-        
-        
         var nb = this.negBleed.x
         ,   v  = this.canvasWidth = w + nb + this.posBleed.x;
-        this.canvas.css({
-            'width' : v+'px',
-            'margin-left' : (-nb)+'px'
-        });
+        this.layerWidth = w;
+        this.layer.width(w).css('margin-left', (-nb)+'px')
+        this.canvas.width(v);
+        // this.canvas.css({
+        //     'width' : v+'px',
+        //     'margin-left' : (-nb)+'px'
+        // });
         this.canvas[0].width = v;
         
         return this;
@@ -176,15 +175,15 @@ Layer = new Y.Class('Layer', {
         if (h === undefined)
             return this.layerHeight;
         
-        this.layerHeight = h;
-        this.layer.height(h);
-        
         var nb = this.negBleed.y
         ,   v  = this.canvasHeight = h + nb + this.posBleed.y;
-        this.canvas.css({
-            'height' : v+'px',
-            'margin-top' : (-nb)+'px'
-        });
+        this.layerHeight = h;
+        this.layer.height(h).css('margin-top', (-nb)+'px')
+        this.canvas.height(v);
+        // this.canvas.css({
+        //     'height' : v+'px',
+        //     'margin-top' : (-nb)+'px'
+        // });
         this.canvas[0].height = v;
         
         return this;
@@ -221,6 +220,27 @@ Layer = new Y.Class('Layer', {
         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'),
     
@@ -265,34 +285,34 @@ Layer = new Y.Class('Layer', {
      * to all sublayers (preserving knowledge of their individual scaling).
      */
     scale : function scale(sx,sy){
+        var o = this.transform.scale;
         if (arguments.length === 0)
-            return { 'x': this.absScaleX, 'y': this.absScaleY };
+            return o.absolute(this.layerWidth, this.layerHeight);
         
         // Record my relative scaling...
+        o.x = sx;
+        o.y = sy;
         this.dirty = true;
-        this.scaleX = sx;
-        this.scaleY = sy;
-        
-        // Propogate...
-        var p = this.parent
-        ,   ps = (p ? p.scale() : { x:1.0, y:1.0 }) ;
-        return this._scale(ps.x, ps.y);
+        return this._applyTransforms();
     },
     
-    _scale : function _scale(parentScaleX, parentScaleY){
-        var absX = this.absScaleX = this.scaleX * parentScaleX
-        ,   absY = this.absScaleY = this.scaleY * parentScaleY
-        ;
-        
-        // Apply absolute scaling...
-        this.ctx.scale(absX, absY);
-        
-        // And propogate down the line
-        this.children.invoke('_scale', absX, absY);
+    /**
+     * Translates draw calls by (x,y) within this layer only. This allows you to
+     * functionally move the coordinate system of the layer.
+     */
+    translate : function translate(x,y){
+        var o = this.transform.translate;
+        if (arguments.length === 0)
+            return o.absolute(this.layerWidth, this.layerHeight);
         
+        // Record my relative scaling...
+        o.x = x;
+        o.y = y;
+        this.dirty = true;
         return this;
     },
     
+    
     // origin    : new Loc('50%','50%'),
     // rotate    : 0,
     // scale     : new Loc(1.0,1.0),
@@ -303,7 +323,7 @@ Layer = new Y.Class('Layer', {
         if (t.rotate !== 0)
             tfns.push('rotate('+t.rotate+'rad)');
         
-        if (t.scale.x !== 1 || t.scale.y !== 1)
+        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')
@@ -389,18 +409,22 @@ Layer = new Y.Class('Layer', {
     
     _openPath : function _openPath(ctx){
         var self = this
-        ,   w = this.canvasWidth
-        ,   h = this.canvasHeight
+        ,   alwaysClear = this.alwaysClear
         ,   neg = this.negBleed
-        ;
+        ,   t = this.transform
+        ,   w = this.canvasWidth, h = this.canvasHeight ;
         
         ctx.beginPath();
         ctx.setTransform(1,0,0,1,0,0);
         ctx.clearRect(-w,-h, 2*w,2*h);
+        
+        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
-        var alwaysClear = !!this.alwaysClear;
         CONTEXT_ATTRS.forEach(function(name){
             if (self[name] !== undefined)
                 ctx[name] = self[name];
@@ -408,10 +432,6 @@ Layer = new Y.Class('Layer', {
                 delete ctx[name];
         });
         
-        // ctx.rotate(this.absRotation);
-        // ctx.translate(-this.originX, -this.originY);
-        // ctx.scale(this.absScaleX, this.absScaleY);
-        
         return this;
     },
     
index abe1250..280fb79 100644 (file)
@@ -36,6 +36,24 @@ math.Line = new Y.Class('Line', math.Vec, {
         return new math.Line(this.x1,this.y1, this.x2,this.y2, this.tdist);
     },
     
+    equals : function equals(line){
+        return ( this.slope === line.slope
+            && this.x1 === line.x1 && this.y1 === line.y1
+            && this.x2 === line.x2 && this.y2 === line.y2 );
+    },
+    
+    intersects : function intersects(x,y){
+        var o = x;
+        if (o instanceof math.Line)
+            return this.slope !== o.slope || this.equals(o);
+        
+        if (o instanceof math.Vec) {
+            x = o.x;
+            y = o.y;
+        }
+        return this.calcY(x) === y;
+    },
+    
     setTScale : function setTScale(tdist){
         if (tdist) {
             this.tdist = tdist;
@@ -77,20 +95,24 @@ math.Line = new Y.Class('Line', math.Vec, {
     },
     
     tangent : function tangent(at){
-        var _theta = Math.PI/2 + this.theta
-        ,   x  = (at.x !== this.x1 ? this.x1 : this.x2) - at.x
-        ,   y  = (at.y !== this.y1 ? this.y1 : this.y2) - at.y
-        ,   _x = at.x + y
-        ,   _y = at.y - x
-        return new math.Line(at.x,at.y, _x,_y, this.tdist);
+        var slope = this.slope;
+        
+        if ( slope === 0 )
+            return new math.Line(at.x,at.y, at.x,at.y+1, this.tdist);
+        
+        if ( !isFinite(slope) )
+            return new math.Line(at.x,at.y, at.x+1,at.y, this.tdist);
+        
+        var x1 = at.x, y1 = at.y
+        ,   x2 = at.x - at.y + (at.y !== this.y1 ? this.y1 : this.y2)
+        ,   y2 = at.y + at.x - (at.x !== this.x1 ? this.x1 : this.x2) ;
+        return new math.Line(x1,y1, x2,y2, this.tdist);
     },
     
     toString : function toString(){
-        return 'Line('+this.x1.toFixed(2)+','+this.y1.toFixed(2)+', '+
-                       this.x2.toFixed(2)+','+this.y2.toFixed(2)+', '+
-                       'slope='+this.slope.toFixed(3)+')';
+        return 'Line( '+this.p1+', '+this.p2+', slope='+this.slope.toFixed(3)+')';
     }
     
 });
 
-})();
+})();
\ No newline at end of file
index 33cf4f2..7ac8e02 100644 (file)
@@ -63,7 +63,7 @@ math.Vec = new Y.Class('Vec', [], {
     },
     
     toString : function toString(){
-        return '['+this.x.toFixed(3)+', '+this.y.toFixed(3)+']';
+        return '('+this.x.toFixed(3)+', '+this.y.toFixed(3)+')';
     }
     
 });
index a31817b..50d3895 100644 (file)
@@ -21,79 +21,18 @@ Shape = new Y.Class('Shape', Layer, {
     
 });
 
-Rect = new Y.Class('Rect', Shape, {
-    _cssClasses : 'portal layer shape rect',
-    
-    init : function initRect(w, h){
-        Layer.init.call(this);
-        
-        this.width(w)
-            .height(h);
-            // .origin(w/2, h/2);
-    },
-    
-    drawShape : function drawShape(ctx){
-        ctx.rect(0,0, this.canvasWidth,this.canvasHeight);
-        ctx.fill();
-    }
-    
-});
 
-Circle = new Y.Class('Circle', Shape, {
-    _cssClasses : 'portal layer shape circle',
-    
-    init : function initCircle(radius){
-        Layer.init.call(this);
-        
-        var d = radius * 2;
-        this.radius = this.negBleed.x = this.negBleed.y = radius;
-        this.width(d).height(d);
-            // .origin(radius,radius);
-    },
-    
-    drawShape : function drawShape(ctx){
-        var r  = this.radius;
-        ctx.arc(0,0, r, 0, Math.PI*2, false);
-        ctx.fill();
-        ctx.stroke();
-    }
+Line = new Y.Class('Line', Shape, {
+    _cssClasses : 'portal layer shape line',
     
-});
-
-Polygon = new Y.Class('Polygon', Shape, {
-    _cssClasses : 'portal layer shape polygon',
+    useCanvasScaling : true,
+    fillStyle   : 'transparent',
+    strokeStyle : "#000000",
+    lineWidth   : 1,
     
-    /**
-     * Expects two arrays of coordinate-halfs, which could be zipped
-     * together to make the numbered coordinates.
-     * x0 and y0 will always be 0.
-     */
-    init : function initPolygon(xs, ys){
-        Layer.init.call(this);
-        
-        var xs = this._calcDimension('x', xs)
-        ,   ys = this._calcDimension('y', ys)
-        ,   w  = Math.max.apply(Math, xs)
-        ,   h  = Math.max.apply(Math, ys)
-        ;
-        
-        this.points = Y(xs).zip(ys).map(Loc.instantiate, Loc);
-        this.width(w)
-            .height(h);
-            // .origin(w/2, h/2);
-    },
+    drawDefinitionPoints : false,
+    invertY : false,
     
-    drawShape : function drawShape(ctx){
-        this.points.forEach(function(loc, i){
-            ctx.lineTo(loc.x, loc.y);
-        });
-        ctx.fill();
-    }
-});
-
-// Er, this won't do. It's only a line-segment.
-Line = new Y.Class('Line', Shape, {
-    _cssClasses : 'portal layer shape line',
     
     init : function initLine(x,y){
         Layer.init.call(this);
@@ -110,25 +49,35 @@ Line = new Y.Class('Line', Shape, {
         if (top === undefined && left === undefined)
             return this.line.p1;
         
-        if (top && Y.isPlainObject(top))
-            var pos = top;
-        else
-            var pos = { 'top': top, 'left':left };
+        var pos = Y.isPlainObject(top) ? top : {'top':top, 'left':left};
         
         this.x1 = pos.left; this.x2 += this.x1;
         this.y1 = pos.top;  this.y2 += this.y1;
-        this.line = new math.Line(this.x1,this.y1, this.x2,this.y2);
+        
+        this.line = new math.Line(this.x1,this.y1, this.x2,this.y2, (this.line||{}).tdist);
         
         return this;
     },
     
+    origin : function origin(x,y){
+        var o = this.transform.origin;
+        if (arguments.length === 0)
+            return o.absolute(this.layerWidth, this.layerHeight);
+        
+        o.x = x;
+        o.y = y;
+        this.ctx.translate(x,y);
+        this.dirty = true;
+        return this._applyTransforms();
+    },
+    
     appendTo : function appendTo(parent){
         var r = Layer.prototype.appendTo.call(this, parent);
-        this.fixSize();
+        this._fixSize();
         return r;
     },
     
-    fixSize : function fixSize(){
+    _fixSize : function _fixSize(){
         var p  = this.parent
         ,   pw = p.canvasWidth,    ph = p.canvasHeight
         ,   w  = this.canvasWidth, h  = this.canvasHeight
@@ -146,30 +95,45 @@ Line = new Y.Class('Line', Shape, {
     },
     
     drawShape : function drawShape(ctx){
-        this.fixSize();
-        var x1,y1, x2,y2
+        this._fixSize();
+        var x1,y1, x2,y2, t = this.transform.translate
         ,   line = this.line, p1 = line.p1, p2 = line.p2
-        ,   w = this.canvasWidth, h = this.canvasHeight
+        ,   minW = -t.x, minH = -t.y
+        ,   maxW = this.canvasWidth+minW, maxH = this.canvasHeight+minH
         ;
         
-        x1 = 0; y1 = line.calcY(x1);
-        if (y1 < 0) {
-            y1 = 0; x1 = line.calcX(y1);
+        x1 = minW; y1 = line.calcY(x1);
+        if (isNaN(y1) || !isFinite(y1)) {
+            y1 = minH; x1 = line.calcX(y1);
         }
         
-        x2 = w; y2 = line.calcY(x2);
-        if (y2 > h) {
-            y2 = h; x2 = line.calcX(y2);
+        x2 = maxW; y2 = line.calcY(x2);
+        if (isNaN(y2) || !isFinite(y2)) {
+            y2 = maxH; x2 = line.calcX(y2);
         }
         
-        ctx.moveTo(x1,y1);
-        ctx.lineTo(x2,y2);
-        ctx.stroke();
-        ctx.closePath();
+        if (this.invertY){
+            y1 = -y1;
+            y2 = -y2;
+        }
+        
+        try {
+            ctx.moveTo(x1,y1);
+            ctx.lineTo(x2,y2);
+            ctx.stroke();
+            ctx.closePath();
+        } catch(e) {
+            console.error(this+'.drawShape()');
+            console.log('  points:', x1,y1, ' ', x2,y2);
+            console.log('  bounds:', minW,minH, ' ', maxW,maxH);
+            console.log('  ::', this, line);
+        }
         
         // Show definition points
-        this.point(p1.x,p1.y, 'rgba(69,150,255,0.4)');
-        this.point(p2.x,p2.y, 'rgba(69,150,255,0.4)');
+        if ( this.drawDefinitionPoints ) {
+            this.point(p1.x,p1.y, 'rgba(69,150,255,0.4)');
+            this.point(p2.x,p2.y, 'rgba(69,150,255,0.4)');
+        }
     },
     
     
@@ -184,6 +148,78 @@ Line.fromPoints = function fromPoints(x1,y1, x2,y2){
 };
 
 
+
+Rect = new Y.Class('Rect', Shape, {
+    _cssClasses : 'portal layer shape rect',
+    
+    init : function initRect(w, h){
+        Layer.init.call(this);
+        
+        this.width(w)
+            .height(h);
+            // .origin(w/2, h/2);
+    },
+    
+    drawShape : function drawShape(ctx){
+        ctx.rect(0,0, this.canvasWidth,this.canvasHeight);
+        ctx.fill();
+    }
+    
+});
+
+Circle = new Y.Class('Circle', Shape, {
+    _cssClasses : 'portal layer shape circle',
+    
+    init : function initCircle(radius){
+        Layer.init.call(this);
+        
+        var d = radius * 2;
+        this.radius = this.negBleed.x = this.negBleed.y = radius;
+        this.width(d).height(d);
+            // .origin(radius,radius);
+    },
+    
+    drawShape : function drawShape(ctx){
+        var r  = this.radius;
+        ctx.arc(0,0, r, 0, Math.PI*2, false);
+        ctx.fill();
+        ctx.stroke();
+    }
+    
+});
+
+Polygon = new Y.Class('Polygon', Shape, {
+    _cssClasses : 'portal layer shape polygon',
+    
+    /**
+     * Expects two arrays of coordinate-halfs, which could be zipped
+     * together to make the numbered coordinates.
+     * x0 and y0 will always be 0.
+     */
+    init : function initPolygon(xs, ys){
+        Layer.init.call(this);
+        
+        var xs = this._calcDimension('x', xs)
+        ,   ys = this._calcDimension('y', ys)
+        ,   w  = Math.max.apply(Math, xs)
+        ,   h  = Math.max.apply(Math, ys)
+        ;
+        
+        this.points = Y(xs).zip(ys).map(Loc.instantiate, Loc);
+        this.width(w)
+            .height(h);
+            // .origin(w/2, h/2);
+    },
+    
+    drawShape : function drawShape(ctx){
+        this.points.forEach(function(loc, i){
+            ctx.lineTo(loc.x, loc.y);
+        });
+        ctx.fill();
+    }
+});
+
+
 Triangle = new Y.Class('Triangle', Polygon, {
     _cssClasses : 'portal layer shape polygon triangle',
     
index da6937a..2c6a179 100644 (file)
@@ -9,21 +9,57 @@
 <style type="text/css" media="screen">
 
 html, body { width:100%; height:100%;
-    font-family:Geogrotesque,Helvetica; color:#999; background-color:#D2E1A7; }
+    font-family:Geogrotesque,Helvetica; color:#333; background-color:#7F7F7F; }
 body { font-family:Geogrotesque, Helvetica; font-size:12pt; }
 h1 { position:fixed; top:0; right:0; margin:0; padding:0; font-size:3em; color:#000; opacity:0.25; z-index:100; }
-ul, ol, li { list-style: none ! important; margin:0; padding:0; }
+/* ul, ol, li { list-style: none ! important; margin:0; padding:0; } */
 
-.rounded { border-radius:1em; -moz-border-radius:1em; -webkit-border-radius:1em; }
+#content { position:relative; width:1000px; margin:1em auto; }
+/* #content > div { margin:1em 0; } */
+
+#plot { width:1000px; height:600px; background-color:#fff; }
+/* #plot .circle { z-index:5; } */
+
+#info { position:absolute; top:0; left:1000px; margin-left:0.5em; width:275px; padding:0.5em;
+    color:#ddd; background-color:rgba(0,0,0, 0.25); line-height:2em; }
+#info fieldset { border:1px solid #444; padding:0.5em; }
+#info legend { color:#ccc; font-weight:bold; padding:0 0.5em; }
+#info input { width:3em; font-size:0.8em; padding:0.1em; text-align:center; background-color:rgba(0,0,0, 0.2); color:#ccc; border:0; }
+#info label { margin-right:1em; }
+
+
+#howto { position:relative; width:600px; margin:1em auto; background-color:#D8D8D8; }
+#howto > * { padding:1em; margin-top:0; margin-bottom:0; }
+#howto h3 { margin:0; padding-bottom:0; }
 
-#plot { position:relative; top:1em; width:1000px; height:600px; margin:0 auto; background-color:#fff; }
-#plot .circle { z-index:5; }
+.handle { cursor:move; }
+.rounded { border-radius:1em; -moz-border-radius:1em; -webkit-border-radius:1em; }
 
 </style>
 </head>
 <body>
-
-<div id="plot"></div>
+<div id="content">
+    <div id="plot"></div>
+    
+    <div id="info" class="rounded">
+        <form id="line" action="#" method="get">
+        <!--<fieldset id="line"><legend>Line</legend>-->
+            <label for="line_coords">Line:</label>
+            ( <input id="line_coords" name="x1" value="" type="text">, <input name="y1" value="" type="text"> )
+            ( <input name="x2" value="" type="text">, <input name="y2" value="" type="text"> )
+        <!--</fieldset>-->
+        </form>
+    </div>
+    
+    <div id="howto" class="rounded">
+        <h3>Trigonometry and Reflection Test</h3>
+        <ul>
+            <li>The main line is pink (and the light-orange line is its tangent).</li>
+            <li>Click anywhere to add a point. It will be purple, and its reflection in the main line will be orange.</li>
+            <li>You can drag the blue control points of the main line to change it.</li>
+        </ul>
+    </div>
+</div>
 
 <div id="scripts">
 <?php
index 5fb18eb..ce7c5a1 100644 (file)
@@ -8,7 +8,7 @@ plot = $('#plot');
 w = plot.width();  w2 = w/2;
 h = plot.height(); h2 = h/2;
 COLS = w/PPU; COLS2 = COLS/2;
-ROWS = h/PPU; H2 = ROWS/2;
+ROWS = h/PPU; ROWS2 = ROWS/2;
 
 grid = new Grid( COLS, ROWS, PPU ).appendTo(plot);
 grid.lineWidth = 1.0;
@@ -17,8 +17,8 @@ grid.draw();
 
 // Draw axes
 ctx = grid.ctx;
-drawLine(-w,-h2, w,-h2, '#CCCCCC', 2.0);
-drawLine(w2,-h,  w2,h, '#CCCCCC', 2.0);
+drawLine(-w,-h2, w, -h2, '#CCCCCC', 2.0);
+drawLine(w2,-h,  w2,  h, '#CCCCCC', 2.0);
 
 P = new Layer()
     .width(w).height(h)
@@ -30,9 +30,10 @@ ctx.scale(PPU, PPU);
 
 points = Y([]);
 line = mkLine(0,0, 2.3125,1);
-rv = addPoint(-5,2);
+addPoint(-5,2);
 
 dragging = false;
+
 P.layer.bind('click', function(evt){
     if (!dragging) {
         var v = convertLoc(evt);
@@ -41,28 +42,29 @@ P.layer.bind('click', function(evt){
     return false;
 });
 
+$('form#line, #line input').bind('submit blur', function(evt){
+    var x1 = parseFloat($('#info [name=x1]').val())
+    ,   y1 = parseFloat($('#info [name=y1]').val())
+    ,   x2 = parseFloat($('#info [name=x2]').val())
+    ,   y2 = parseFloat($('#info [name=y2]').val())
+    ;
+    
+    if ( !(isNaN(x1) || isNaN(y1)) && (line.x1 !== x1 || line.y1 !== y1) )
+        mvLine(1, x1,y1);
+    
+    if ( !(isNaN(x2) || isNaN(y2)) && (line.x2 !== x2 || line.y2 !== y2) )
+        mvLine(2, x2,y2);
+    
+    return false;
 });
 
-function convertLoc(evt){
-    var off = P.layer.offset()
-    ,   x   = (evt.pageX - off.left - w2)/PPU
-    ,   y   = (evt.pageY - off.top  - h2)/PPU * -1;
-    return new math.Vec(x,y);
-}
+});
 
-function clear(){
-    ctx.clearRect(-2*w,-2*h, 4*w,4*h);
-    points.forEach(removePoint);
-    p1.remove(); p2.remove();
-}
-
-function mvLine(pn, x,y){
-    var x1 = (pn === 1 ? x : line.x1)
-    ,   y1 = (pn === 1 ? y : line.y1)
-    ,   x2 = (pn === 2 ? x : line.x2)
-    ,   y2 = (pn === 2 ? y : line.y2)
-    ;
-    mkLine(x1,y1, x2,y2);
+function updateInfo(){
+    $('#info [name=x1]').val(line.x1);
+    $('#info [name=y1]').val(line.y1);
+    $('#info [name=x2]').val(line.x2);
+    $('#info [name=y2]').val(line.y2);
 }
 
 function redraw(color){
@@ -71,20 +73,17 @@ function redraw(color){
     clear();
     
     // drawAngle(line.theta);
-    drawLine(-COLS, line.calcY(-COLS), COLS, line.calcY(COLS), color);
-    drawLine(-COLS, tanLine.calcY(-COLS), COLS, tanLine.calcY(COLS), 'rgba(226,127,8, 0.5)');
     
-    P.append(p1, p2);
+    // drawLine(-COLS, line.calcY(-COLS), COLS, line.calcY(COLS), color);
+    // drawLine(-COLS, tan.calcY(-COLS), COLS, tan.calcY(COLS), 'rgba(226,127,8, 0.2)');
+    
+    P.append(oline, otan, p1, p2).draw();
     bindPoint(p1, 1);
     bindPoint(p2, 2);
     
     pts.forEach(addPoint);
-}
-
-function removePoint(c){
-    if (c.reflected) c.reflected.remove();
-    points.remove(c);
-    c.remove();
+    
+    updateInfo();
 }
 
 function addPoint(x,y){
@@ -100,23 +99,45 @@ function addPoint(x,y){
     var rv = math.reflect(c.vec,line)
     ,   rc = c.reflected = drawPoint(rv, null, '#F25522');
     points.push(c);
-    return rv;
+    return c;
 }
 
-function mkLine(x1,y1, x2,y2, color, pcolor){
-    if (window.p1) p1.remove();
-    if (window.p2) p2.remove();
-    
-    color  = color  || 'rgba(231,48,117, 0.5)';
-    pcolor = pcolor || 'rgba(69,150,255, 1)';
+function removePoint(c){
+    if (c.reflected) c.reflected.remove();
+    points.remove(c);
+    c.remove();
+}
+
+function mkLine(x1,y1, x2,y2){
+    var lcolor = 'rgba(231,48, 117, 0.5)'
+    ,   tcolor = 'rgba(226,127,8,   0.5)'
+    ,   pcolor = 'rgba(69, 150,255, 1.0)';
     
-    line = new math.Line(x1,y1, x2,y2);
-    tanLine = line.tangent(line.p2);
+    if (window.oline) oline.remove();
+    oline = Line.fromPoints(x1,y1, x2,y2)
+                .attr('invertY', true)
+                .stroke(lcolor, PX)
+                .translate(COLS2, ROWS2)
+                .scale(PPU, PPU);
+    line  = oline.line;
+    oline.layer.attr('title', line+'');
     
-    // drawLine(-COLS, line.calcY(-COLS), COLS, line.calcY(COLS), color);
+    if (window.otan) otan.remove();
+    tan   = line.tangent(line.p2);
+    otan  = Line.fromPoints(tan.x1,tan.y1, tan.x2,tan.y2)
+                .attr('invertY', true)
+                .stroke(tcolor, PX)
+                .translate(COLS2, ROWS2)
+                .scale(PPU, PPU);
+    otan.layer.attr('title', tan+'');
     
+    if (window.p1) p1.remove();
     p1 = drawPoint(line.x1, line.y1, pcolor);
+    
+    if (window.p2) p2.remove();
     p2 = drawPoint(line.x2, line.y2, pcolor);
+    
+    p1.layer.add(p2.layer).addClass('handle');
     redraw();
     
     return line;
@@ -141,9 +162,35 @@ function bindPoint(pt, pn){
     });
     return pt;
 }
+function convertLoc(evt){
+    var off = P.layer.offset()
+    ,   x   = (evt.pageX - off.left - w2)/PPU
+    ,   y   = (evt.pageY - off.top  - h2)/PPU * -1;
+    return new math.Vec(x,y);
+}
+
+function clear(){
+    ctx.clearRect(-2*w,-2*h, 4*w,4*h);
+    points.forEach(removePoint);
+    p1.remove(); p2.remove();
+}
+
+function mvLine(pn, x,y){
+    var x1 = (pn === 1 ? x : line.x1)
+    ,   y1 = (pn === 1 ? y : line.y1)
+    ,   x2 = (pn === 2 ? x : line.x2)
+    ,   y2 = (pn === 2 ? y : line.y2)
+    ;
+    mkLine(x1,y1, x2,y2);
+}
+
+
+
+
+
 
 function drawLine(x1,y1, x2,y2, color, width){
-    // try {
+    try {
         ctx.beginPath();
         ctx.lineWidth = width || PX;
         ctx.strokeStyle = color || '#000000';
@@ -151,9 +198,9 @@ function drawLine(x1,y1, x2,y2, color, width){
         ctx.lineTo(x2, -y2);
         ctx.stroke();
         ctx.closePath();
-    // } catch (e) {
-    //     console.log('drawLine error:', e, '(',x1,y1,')', '(',x2,y2,')');
-    // }
+    } catch (e) {
+        window.console && console.log('drawLine error:', e, '(',x1,y1,')', '(',x2,y2,')');
+    }
 }
 
 function drawPoint(x,y, color, r){
@@ -168,7 +215,7 @@ function drawPoint(x,y, color, r){
     var c = new Circle(r)
         .position(w2 + x*PPU, h2 - y*PPU)
         .attr({
-            'strokeStyle' : '',
+            'strokeStyle' : 'transparent',
             'fillStyle'   : color || 'rgba(0,0,0,0.5)'
         })
         .appendTo(P)
@@ -182,11 +229,10 @@ function drawPoint(x,y, color, r){
 function drawAngle(theta, radius){
     radius = radius || 3;
     ctx.beginPath();
-    ctx.strokeStyle = '';
+    ctx.strokeStyle = 'transparent';
     ctx.fillStyle = 'rgba(36,71,146, 0.25)';
     ctx.moveTo(0,0);
     ctx.lineTo(radius,0);
-    // ctx.arc(0,0, radius, 0, -(Math.PI/2-theta), true);
     ctx.arc(0,0, radius, 0, -theta, true);
     ctx.lineTo(0,0);
     ctx.fill();