/*

    Copyright 2010 Brad Christie

    This file is part of TAMinations.

    TAMinations is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    TAMinations is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with TAMinations.  If not, see <http://www.gnu.org/licenses/>.

 */

/*

  TamSVG - Javascript+SVG implementation of the original TAMination.java

*/

// Use jQuery to make a deep copy
function clone(obj)
{
  return jQuery.extend(true,{},obj);
}

//  Setup - called when page is loaded
function TamSVG(svg_in)
{
  if (this instanceof TamSVG)
    this.init(svg_in);
  else
    window.tamsvg = new TamSVG(svg_in);
}
TamSVG.prototype = {
  init: function(svg_in)
  {
    var me = this;
    var cookie = new Cookie(document,"TAMination",365*24,'/');
    cookie.load();
    //  Get initial values from cookie
    //  This is a hook to test hexagon
    this.hexagon = cookie.hexagon == "true" || args.hexagon == 'true';
    this.loop = cookie.loop == "true";
    this.showPaths = cookie.paths == "true";
    this.grid = cookie.grid == "true";
    this.showPhantoms = cookie.phantoms == "true";
    if (cookie.svg != 'true') {
      cookie.svg = "true";
      cookie.store();
    }
    this.currentpart = 0;
    //  Set up the dance floor
    this.svg = svg_in;
    this.svg.configure({viewBox: '0 0 100 100'});
    floorsvg = this.svg.svg(null,0,0,100,100,-6.5,-6.5,13,13);
    this.allp = tam.getPath(tam.xmldoc);
    this.parts = tam.getParts().split(/;/);
    for (var i in this.parts)
      this.parts[i] = Number(this.parts[i]);
    //  first token is 'Formation', followed by e.g. boy 1 2 180 ...
    var tokens = getFormation(tam.callnum).split(/\s+/);
    //  Flip the y direction on the dance floor to match our math
    this.floor = this.svg.group(floorsvg);
    this.floor.setAttribute('transform',AffineTransform.getScaleInstance(1,-1).toString());
    this.svg.rect(this.floor,-6.5,-6.5,13,13,{fill:'#ffffc0'});
    this.svg.text(floorsvg,0,0,"Copyright 2010 Brad Christie",{fontSize: "10", transform:"translate(-6.5,6.4) scale(0.04)"});
    this.gridgroup = this.svg.group(this.floor,{fill:"none",stroke:"black",strokeWidth:0.01});
    this.hexgridgroup = this.svg.group(this.floor,{fill:"none",stroke:"black",strokeWidth:0.01});
    this.drawGrid();
    if (!this.grid || this.hexagon)
      this.gridgroup.setAttribute('visibility','hidden');
    if (!this.grid || !this.hexagon)
      this.hexgridgroup.setAttribute('visibility','hidden');
    this.pathparent = this.svg.group(this.floor);
    this.pathgroup = this.svg.group(this.pathparent);
    if (!this.showPaths)
      this.pathgroup.setAttribute('visibility','hidden');
    this.handholds = this.svg.group(this.floor);
    this.dancegroup = this.svg.group(this.floor);
    this.dancers = [];
    var dancerColor = [ Color.red, Color.yellow, Color.lightGray ];
    for (var i=1; i<tokens.length; i+=4) {
      var d = new Dancer(this,Dancer.genders[tokens[i]],
              -Number(tokens[i+2]),-Number(tokens[i+1]),
              Number(tokens[i+3])+180,
              dancerColor[i>>3],this.allp[i>>2]);
      if (d.gender == Dancer.PHANTOM && !this.showPhantoms)
        d.hide();
      this.dancers.push(d);
      d = new Dancer(this,Dancer.genders[tokens[i]],
              Number(tokens[i+2]),Number(tokens[i+1]),
              Number(tokens[i+3]),
              dancerColor[i>>3].rotate(),this.allp[i>>2]);
      if (d.gender == Dancer.PHANTOM && !this.showPhantoms)
        d.hide();
      this.dancers.push(d);
    }
    //  Compute animation length
    this.beats = 0.0;
    for (var d in this.dancers)
      this.beats = Math.max(this.beats,this.dancers[d].beats());
    this.beats += 2.0;
    //  Set up the button/slider panel
    $('#appletcontainer').append('<div id="buttonpanel" style="background-color: #c0c0c0"></div>');

    $('#buttonpanel').append('<div id="optionpanel"></div>');
    $('#optionpanel').append('<input type="button" class="appButton" id="slowButton" value="Slow"/>');
    $('#optionpanel').append('<input type="button" class="appButton selected" id="normalButton" value="Normal"/>');
    $('#optionpanel').append('<input type="button" class="appButton" id="fastButton" value="Fast"/>');
    $('#optionpanel').append('<input type="button" class="appButton" id="loopButton" value="Loop"/>');
    if (this.loop)
      $('#loopButton').addClass('selected');
    $('#optionpanel').append('<input type="button" class="appButton" id="pathButton" value="Paths"/>');
    if (this.showPaths)
      $('#pathButton').addClass('selected');
    $('#optionpanel').append('<input type="button" class="appButton" id="gridButton" value="Grid"/>');
    if (this.grid)
      $('#gridButton').addClass('selected');
    if (this.dancers.length > 8) {
      $('#optionpanel').append('<input type="button" class="appButton" id="phantomButton" value="Phantoms"/>');
      if (this.showPhantoms)
        $('#phantomButton').addClass('selected');
    } else {
      $('#optionpanel').append('<input type="button" class="appButton" id="hexButton" value="Hex"/>');
      if (this.hexagon)
        $('#hexButton').addClass('selected');
    }

    //  Speed button actions
    $('#slowButton').click(function() {
      me.slow();
      $('#slowButton').addClass('selected');
      $('#normalButton,#fastButton').removeClass('selected');
    });
    $('#normalButton').click(function() {
      me.normal();
      $('#normalButton').addClass('selected');
      $('#slowButton,#fastButton').removeClass('selected');
    });
    $('#fastButton').click(function() {
      me.fast();
      $('#fastButton').addClass('selected');
      $('#slowButton,#normalButton').removeClass('selected');
    });

    //  Actions for other options
    $('#loopButton').click(function() {
      if (me.loop) {
        me.loop = false;
        $('#loopButton').removeClass('selected');
      } else {
        me.loop = true;
        $('#loopButton').addClass('selected');
      }
      cookie.loop = me.loop;
      cookie.store();
    });
    $('#pathButton').click(function() {
      if (me.showPaths) {
        me.showPaths = false;
        me.pathgroup.setAttribute('visibility','hidden');
        $('#pathButton').removeClass('selected');
      } else {
        me.showPaths = true;
        me.pathgroup.setAttribute('visibility','visible');
        $('#pathButton').addClass('selected');
      }
      cookie.paths = me.showPaths;
      cookie.store();
    });
    $('#gridButton').click(function() {
      if (me.grid) {
        me.grid = false;
        me.hexgridgroup.setAttribute('visibility','hidden');
        me.gridgroup.setAttribute('visibility','hidden');
        $('#gridButton').removeClass('selected');
      } else {
        me.grid = true;
        if (me.hexagon)
          me.hexgridgroup.setAttribute('visibility','visible');
        else
          me.gridgroup.setAttribute('visibility','visible');
        $('#gridButton').addClass('selected');
      }
      cookie.grid = me.grid;
      cookie.store();
    });
    $('#hexButton').click(function() {
      //alert('Sorry, svg hexagon animations not ready yet.');
      //return;
      if (me.hexagon) {
        me.hexagon = false;
        me.revertFromHexagon();
        me.animate();
        $('#hexButton').removeClass('selected');
        if (me.grid) {
          me.hexgridgroup.setAttribute('visibility','hidden');
          me.gridgroup.setAttribute('visibility','visible');
        }
      } else {
        me.hexagon = true;
        me.convertToHexagon();
        me.animate();
        $('#hexButton').addClass('selected');
        if (me.grid) {
          me.gridgroup.setAttribute('visibility','hidden');
          me.hexgridgroup.setAttribute('visibility','visible');
        }
      }
      me.svg.remove(me.pathgroup);
      me.pathgroup = me.svg.group(me.pathparent);
      if (!me.showPaths)
        me.pathgroup.setAttribute('visibility','hidden');
      for (var i in me.dancers)
        me.dancers[i].paintPath();
      cookie.hexagon = me.hexagon;
      cookie.store();
    });
    $('#phantomButton').click(function() {
      if (me.showPhantoms) {
        me.showPhantoms = false;
        for (var i in me.dancers)
          if (me.dancers[i].gender == Dancer.PHANTOM)
            me.dancers[i].hide();
        me.animate();
        $('#phantomButton').removeClass('selected');
      } else {
        me.showPhantoms = true;
        for (var i in me.dancers)
          if (me.dancers[i].gender == Dancer.PHANTOM)
            me.dancers[i].show();
        me.animate();
        $('#phantomButton').addClass('selected');
      }
    });

    //  Slider
    $('#buttonpanel').append('<div id="playslider" style="margin:10px 10px 0 10px"></div>');
    $('#playslider').slider({min: -200, max: this.beats*100, value: -200,
      slide: function(event,ui) {
      me.beat = ui.value/100;
      me.lastPaintTime = new Date().getTime();
      me.animate();
    }});
    //  Slider tick marks
    $('#buttonpanel').append('<div id="playslidertics" style="position: relative; height:10px; width:100%"></div>');
    for (var i=-1; i<this.beats; i++) {
      var x = (i+2) * $('#buttonpanel').width() / (this.beats+2);
      $('#playslidertics').append('<div style="position: absolute; background-color: black; top:0; left:'+x+'px; height:100%; width: 1px"></div>');
    }
    //  "Start", "End" and part numbers below slider
    $('#buttonpanel').append('<div id="playsliderlegend" style="color: black; position: relative; top:0; left:0; width:100%; height:16px"></div>');
    var startx = 2 * $('#buttonpanel').width() / (this.beats+2) - 50;
    var endx = this.beats * $('#buttonpanel').width() / (this.beats+2) - 50;
    $('#playsliderlegend').append('<div style="position:absolute; top:0; left:'+startx+'px; width:100px; text-align: center">Start</div>');
    $('#playsliderlegend').append('<div style="position:absolute;  top:0; left:'+endx+'px; width: 100px; text-align:center">End</div>');
    var offset = 0;
    for (var i in this.parts) {
      var t = '<font size=-2><sup>'+(Number(i)+1) + '</sup>/<sub>' + (this.parts.length+1) + '</sub></font>';
      offset += this.parts[i];
      var x = (offset+2) * $('#buttonpanel').width() / (this.beats+2) - 20;
      $('#playsliderlegend').append('<div style="position:absolute; top:0; left:'+x+
          'px; width:40px; text-align: center">'+t+'</div>');
    }

    //  Bottom row of buttons
    $('#buttonpanel').append('<input type="button" class="appButton" id="rewindButton" value="&lt;&lt;"/>');
    $('#rewindButton').click(function() { me.rewind(); });
    $('#buttonpanel').append('<input type="button" class="appButton" id="prevButton" value="|&lt;"/>');
    $('#prevButton').click(function() { me.prev(); });
    $('#buttonpanel').append('<input type="button" class="appButton" id="backButton" value="&lt;"/>');
    $('#backButton').click(function() { me.backward(); });
    $('#buttonpanel').append('<input type="button" class="appButton" id="playButton" value="Play"/>');
    $('#playButton').click(function() { me.play(); });
    $('#buttonpanel').append('<input type="button" class="appButton" id="forwardButton" value="&gt;"/>');
    $('#forwardButton').click(function() { me.forward(); });
    $('#buttonpanel').append('<input type="button" class="appButton" id="nextButton" value="&gt;|"/>');
    $('#nextButton').click(function() { me.next(); });
    $('#buttonpanel').append('<input type="button" class="appButton" id="endButton" value="&gt;&gt;"/>');
    $('#endButton').click(function() { me.end(); });


    //  Initialize the animation
    if (this.hexagon)
      this.convertToHexagon();
    this.beat = -2.0;
    this.speed = 500;
    this.running = false;
    this.animate();
  },

  //  This function is called repeatedly to move the dancers
  animate: function()
  {
    //  Update the animation time
    now = new Date().getTime();
    diff = now - this.lastPaintTime;
    if (this.running)
      this.beat += diff/this.speed;
    //  Update the dancers
    this.paint();
    this.lastPaintTime = now;
    //  Update the slider
    $('#playslider').slider('value',this.beat*100);
    if (this.beat >= this.beats) {
      if (this.loop)
        this.beat = -2;
      else
        this.stop();
    }
    //  Update the definition highlight
    var thispart = 1;
    var partsum = 0;
    for (var i in this.parts) {
      if (this.parts[i]+partsum < this.beat)
        thispart++
      partsum += this.parts[i];
    }
    if (this.beat < 0 || this.beat > this.beats-2)
      thispart = 0;
    if (thispart != this.currentpart) {
      setPart(thispart);
      this.currentpart = thispart;
    }
  },

  paint: function()
  {
    //  Move dancers
    for (var i in this.dancers)
      this.dancers[i].animate(this.beat);

    //  Compute handholds
    Handhold.dfactor0 = this.hexagon ? 1.15 : 1.0;
    var hhlist = [];
    for (var i0 in this.dancers) {
      var d0 = this.dancers[i0];
      d0.rightdancer = d0.leftdancer = null;
      d0.rightHandNewVisibility = false;
      d0.leftHandNewVisibility = false;
    }
    for (var i1=0; i1<this.dancers.length-1; i1++) {
      var d1 = this.dancers[i1];
      if (d1.gender==Dancer.PHANTOM && !this.showPhantoms)
        continue;
      for (var i2=i1+1; i2<this.dancers.length; i2++) {
        var d2 = this.dancers[i2];
        if (d2.gender==Dancer.phantom && !this.showPhantoms)
          continue;
        var hh = Handhold.getHandhold(d1,d2);
        if (hh != null)
          hhlist.push(hh);
      }
    }

    hhlist.sort(function(a,b) { return a.score - b.score; });
    for (var h in hhlist) {
      var hh = hhlist[h];
      //  Check that the hands aren't already used
      var incenter = this.hexagon && hh.inCenter();
      if (incenter ||
          (hh.h1 == Movement.RIGHTHAND && hh.d1.rightdancer == null ||
              hh.h1 == Movement.LEFTHAND && hh.d1.leftdancer == null) &&
              (hh.h2 == Movement.RIGHTHAND && hh.d2.rightdancer == null ||
                  hh.h2 == Movement.LEFTHAND && hh.d2.leftdancer == null)) {
        hh.paint();
        if (incenter)
          continue;
        if (hh.h1 == Movement.RIGHTHAND) {
          hh.d1.rightdancer = hh.d2;
          if ((hh.d1.hands & Movement.GRIPRIGHT) == Movement.GRIPRIGHT)
            hh.d1.rightgrip = hh.d2;
        } else {
          hh.d1.leftdancer = hh.d2;
          if ((hh.d1.hands & Movement.GRIPLEFT) == Movement.GRIPLEFT)
            hh.d1.leftgrip = hh.d2;
        }
        if (hh.h2 == Movement.RIGHTHAND) {
          hh.d2.rightdancer = hh.d1;
          if ((hh.d2.hands & Movement.GRIPRIGHT) == Movement.GRIPRIGHT)
            hh.d2.rightgrip = hh.d1;
        } else {
          hh.d2.leftdancer = hh.d1;
          if ((hh.d2.hands & Movement.GRIPLEFT) == Movement.GRIPLEFT)
            hh.d2.leftgrip = hh.d1;
        }
      }
    }
    //  Clear handholds no longer visible
    for (var i in this.dancers) {
      var d = this.dancers[i];
      if (d.rightHandVisibility && !d.rightHandNewVisibility) {
        d.righthand.setAttribute('visibility','hidden');
        d.rightHandVisibility = false;
      }
      if (d.leftHandVisibility && !d.leftHandNewVisibility) {
        d.lefthand.setAttribute('visibility','hidden');
        d.leftHandVisibility = false;
      }
    }

    //  Paint dancers with hands
    for (var i in this.dancers)
      this.dancers[i].paint();
  },

  rewind: function()
  {
    this.stop();
    this.beat = -2;
    this.animate();
  },

  prev: function()
  {
    var b = 0;
    var best = this.beat;
    for (var i in this.parts) {
      b += this.parts[i];
      if (b < this.beat)
        best = b;
    }
    if (best == this.beat && best > 0)
      best = 0;
    else if (this.beat <= 0)
      best = -2;
    this.beat = best;
    this.animate();
  },

  backward: function()
  {
    this.stop();
    if (this.beat > 0.1)
      this.beat -= 0.1;
    this.animate();
  },

  stop: function()
  {
    if (this.timer != null)
      clearInterval(this.timer);
    this.timer = null;
    this.running = false;
    $('#playButton').attr('value','Play');
  },

  start: function()
  {
    this.lastPaintTime = new Date().getTime();
    if (this.timer == null) {
      var me = this;
      this.timer = setInterval(function() { me.animate(); },25);
    }
    if (this.beat >= this.beats)
      this.beat = -2;
    this.running = true;
    $('#playButton').attr('value','Stop');
  },

  play: function()
  {
    if (this.running)
      this.stop();
    else
      this.start();
  },

  forward: function()
  {
    this.stop();
    if (this.beat < this.beats)
      this.beat += 0.1;
    this.animate();
  },

  next: function()
  {
    var b = 0;
    for (var i in this.parts) {
      b += this.parts[i];
      if (b > this.beat) {
        this.beat = b;
        b = -1000;
      }
    }
    if (b >= 0 && b < this.beats-2)
      this.beat = this.beats-2;
    this.animate();
  },

  end: function()
  {
    this.stop();
    if (this.beat < this.beats-2)
      this.beat = this.beats-2;
    this.animate();
  },

  slow: function()
  {
    this.speed = 1500;
  },
  normal: function()
  {
    this.speed = 500;
  },
  fast: function()
  {
    this.speed = 200;
  },

  drawGrid: function()
  {
    //  Square grid
    for (var x=-7.5; x<=7.5; x+=1)
      this.svg.line(this.gridgroup,x,-7.5,x,7.5);
    for (var y=-7.5; y<=7.5; y+=1)
      this.svg.line(this.gridgroup,-7.5,y,7.5,y);
    //  Hex grid
    for (var x0=0.5; x0<=8.5; x0+=1) {
      var points = [];
      // moveto 0, x0
      points.push([0,x0]);
      for (var y0=0.5; y0<=8.5; y0+=0.5) {
        var a = Math.atan2(y0,x0)*2/3;
        var r = Math.sqrt(x0*x0+y0*y0);
        var x = r*Math.sin(a);
        var y = r*Math.cos(a);
        // lineto x,y
        points.push([x,y]);
      }
      //  reflect and rotate the result
      for (var a=0; a<6; a++) {
        var t = "rotate("+(a*60)+")";
        this.svg.polyline(this.hexgridgroup,points,{transform:t});
        this.svg.polyline(this.hexgridgroup,points,{transform:t+" scale(1,-1)"});
      }
    }
  },

  convertToHexagon: function()
  {
    //  Save current dancers
    for (var i in this.dancers)
      this.dancers[i].hide();
    this.saveDancers = this.dancers;
    this.dancers = [];
    var dancerColor = [ Color.red, Color.yellow, Color.green, Color.blue,
                        Color.magenta, Color.cyan ];
    for (var i=0; i<this.saveDancers.length; i+=2) {
      var j = Math.floor(i/4);
      this.dancers.push(new Dancer(this.saveDancers[i],0,0,0,0,dancerColor[j]));
      this.dancers.push(new Dancer(this.saveDancers[i],0,0,0,120,dancerColor[j+2]));
      this.dancers.push(new Dancer(this.saveDancers[i],0,0,0,240,dancerColor[j+4]));
    }
    //  Generate hexagon dancers
    for (var i=0; i<this.dancers.length; i++) {
      this.hexagonify(this.dancers[i],(i%3)*120);
      this.dancers[i].hexagonify();
    }
  },

  revertFromHexagon: function()
  {
    for (var i in this.dancers)
      this.dancers[i].hide();
    this.dancers = this.saveDancers;
    for (var i in this.dancers)
      this.dancers[i].show();
    this.animate();
  },

  //  Moves the position and angle of a dancer from square to hexagon
  hexagonify: function(d,a)
  {
    a = a*Math.PI/180;
    var x = d.startx;
    var y = d.starty;
    var r = Math.sqrt(x*x+y*y);
    var angle = Math.atan2(y,x);
    var dangle = 0.0;
    if (angle < 0)
      dangle = -(Math.PI+angle)/3;
    else
      dangle = (Math.PI-angle)/3;
    d.startx = r*Math.cos(angle+dangle+a+Math.PI*30/180);
    d.starty = r*Math.sin(angle+dangle+a+Math.PI*30/180);
    d.startangle += 30+dangle*180/Math.PI;

    d.computeStart();
    d.recalculate();
  }


};
////////////////////////////////////////////////////////////////////////////////
//  Handhold class for computing the potential handhold between two dancers
//  The actual graphic hands are part of the Dancer object

//  Properties of Handhold object
//  Dancer d1,d2;
//  int h1,h2;
//  angle ah1, ah2; (in radians)
//  double score;
//  private boolean isincenter = false;
//  public static double dfactor0 = 1.0;

function Handhold(/*Dancer*/ dd1, /*Dancer*/ dd2,
         /*int*/ hh1, /*int*/ hh2, /*angle*/ ahh1, ahh2, /*distance*/ d, s)
{
  this.d1 = dd1;
  this.d2 = dd2;
  this.h1 = hh1;
  this.h2 = hh2;
  this.ah1 = ahh1;
  this.ah2 = ahh2;
  this.distance = d;
  this.score = s;
}

  //  If two dancers can hold hands, create and return a handhold.
  //  Else return null.
Handhold.getHandhold = function(/*Dancer*/ d1, /*Dancer*/ d2)
{
  if (d1.hidden || d2.hidden)
    return null;
  //  Turn off grips if not specified in current movement
  if ((d1.hands & Movement.GRIPRIGHT) != Movement.GRIPRIGHT)
    d1.rightgrip = null;
  if ((d1.hands & Movement.GRIPLEFT) != Movement.GRIPLEFT)
    d1.leftgrip = null;
  if ((d2.hands & Movement.GRIPRIGHT) != Movement.GRIPRIGHT)
    d2.rightgrip = null;
  if ((d2.hands & Movement.GRIPLEFT) != Movement.GRIPLEFT)
    d2.leftgrip = null;


  //  Check distance
  var x1 = d1.tx.getTranslateX();
  var y1 = d1.tx.getTranslateY();
  var x2 = d2.tx.getTranslateX();
  var y2 = d2.tx.getTranslateY();
  var dx = x2-x1;
  var dy = y2-y1;
  var dfactor1 = 0.1;  // for distance up to 2.0
  var dfactor2 = 2.0;  // for distance past 2.0
  var d = Math.sqrt(dx*dx+dy*dy);
  var d0 = d*Handhold.dfactor0;
  var score1 = d0 > 2.0 ? (d0-2.0)*dfactor2+2*dfactor1 : d0*dfactor1;
  var score2 = score1;
  //  Angle between dancers
  var a0 = Math.atan2(dy,dx);
  //  Angle each dancer is facing
  var a1 = Math.atan2(d1.tx.getShearY(),d1.tx.getScaleY());
  var a2 = Math.atan2(d2.tx.getShearY(),d2.tx.getScaleY());
  //  For each dancer, try left and right hands
  var h1 = 0;
  var h2 = 0;
  var ah1 = 0;
  var ah2 = 0;
  var afactor1 = 0.2;
  var afactor2 = 1.0;
  //  Dancer 1
  var a = Math.abs(Math.IEEEremainder(Math.abs(a1-a0+Math.PI*3/2),Math.PI*2));
  var ascore = a > Math.PI/6 ? (a-Math.PI/6)*afactor2+Math.PI/6*afactor1
                                : a*afactor1;
  if (score1+ascore < 1.0 && (d1.hands & Movement.RIGHTHAND) != 0 &&
      d1.rightgrip==null || d1.rightgrip==d2) {
    score1 = d1.rightgrip==d2 ? 0.0 : score1 + ascore;
    h1 = Movement.RIGHTHAND;
    ah1 = a1-a0+Math.PI*3/2;
  } else {
    a = Math.abs(Math.IEEEremainder(Math.abs(a1-a0+Math.PI/2),Math.PI*2));
    ascore = a > Math.PI/6 ? (a-Math.PI/6)*afactor2+Math.PI/6*afactor1
                           : a*afactor1;
    if (score1+ascore < 1.0 && (d1.hands & Movement.LEFTHAND) != 0 &&
        d1.leftgrip==null || d1.leftgrip==d2) {
      score1 = d1.leftgrip==d2 ? 0.0 : score1 + ascore;
      h1 = Movement.LEFTHAND;
      ah1 = a1-a0+Math.PI/2;
    } else
      score1 = 10;
  }
  //  Dancer 2
  a = Math.abs(Math.IEEEremainder(Math.abs(a2-a0+Math.PI/2),Math.PI*2));
  ascore = a > Math.PI/6 ? (a-Math.PI/6)*afactor2+Math.PI/6*afactor1
                         : a*afactor1;
  if (score2+ascore < 1.0 && (d2.hands & Movement.RIGHTHAND) != 0 &&
      d2.rightgrip==null || d2.rightgrip==d1) {
    score2 = d2.rightgrip==d1 ? 0.0 : score2 + ascore;
    h2 = Movement.RIGHTHAND;
    ah2 = a2-a0+Math.PI/2;
  } else {
    a = Math.abs(Math.IEEEremainder(Math.abs(a2-a0+Math.PI*3/2),Math.PI*2));
    ascore = a > Math.PI/6 ? (a-Math.PI/6)*afactor2+Math.PI/6*afactor1
                           : a*afactor1;
    if (score2+ascore < 1.0 && (d2.hands & Movement.LEFTHAND) != 0 &&
        d2.leftgrip==null || d2.leftgrip==d1) {
      score2 = d2.leftgrip==d1 ? 0.0 : score2 + ascore;
      h2 = Movement.LEFTHAND;
      ah2 = a2-a0+Math.PI*3/2;
    } else
      score2 = 10;
  }

  if (d1.rightgrip == d2 && d2.rightgrip == d1)
    return new Handhold(d1,d2,Movement.RIGHTHAND,Movement.RIGHTHAND,ah1,ah2,d,0);
  if (d1.rightgrip == d2 && d2.leftgrip == d1)
    return new Handhold(d1,d2,Movement.RIGHTHAND,Movement.LEFTHAND,ah1,ah2,d,0);
  if (d1.leftgrip == d2 && d2.rightgrip == d1)
    return new Handhold(d1,d2,Movement.LEFTHAND,Movement.RIGHTHAND,ah1,ah2,d,0);
  if (d1.leftgrip == d2 && d2.leftgrip == d1)
    return new Handhold(d1,d2,Movement.LEFTHAND,Movement.LEFTHAND,ah1,ah2,d,0);

  if (score1 > 1.0 || score2 > 1.0 || score1+score2 > 1.0)
    return null;
  //window.alert(score1+" "+score2);
  return new Handhold(d1,d2,h1,h2,ah1,ah2,d,score1+score2);
}

/* boolean */
Handhold.prototype.inCenter = function()
{
  var x1 = this.d1.tx.getTranslateX();
  var y1 = this.d1.tx.getTranslateY();
  var x2 = this.d2.tx.getTranslateX();
  var y2 = this.d2.tx.getTranslateY();
  this.isincenter = Math.sqrt(x1*x1+y1*y1) < 1.1 &&
         Math.sqrt(x2*x2+y2*y2) < 1.1;
  return this.isincenter;
}

//  Make the handhold visible
Handhold.prototype.paint = function()
{
  //  Scale should be 1 if distance is 2
  var scale = this.distance/2;
  if (this.h1 == Movement.RIGHTHAND || this.h1 == Movement.GRIPRIGHT) {
    if (!this.d1.rightHandVisibility) {
      this.d1.righthand.setAttribute('visibility','visible');
      this.d1.rightHandVisibility = true;
    }
    this.d1.rightHandNewVisibility = true;
    this.d1.rightHandTransform = AffineTransform.getRotateInstance(-this.ah1)
      .concatenate(AffineTransform.getScaleInstance(scale,scale));
  }
  if (this.h1 == Movement.LEFTHAND || this.h1 == Movement.GRIPLEFT) {
    if (!this.d1.leftHandVisibility) {
      this.d1.lefthand.setAttribute('visibility','visible');
      this.d1.leftHandVisibility = true;
    }
    this.d1.leftHandNewVisibility = true;
    this.d1.leftHandTransform = AffineTransform.getRotateInstance(-this.ah1)
      .concatenate(AffineTransform.getScaleInstance(scale,scale));
  }
  if (this.h2 == Movement.RIGHTHAND || this.h2 == Movement.GRIPRIGHT) {
    if (!this.d2.rightHandVisibility) {
      this.d2.righthand.setAttribute('visibility','visible');
      this.d2.rightHandVisibility = true;
    }
    this.d2.rightHandNewVisibility = true;
    this.d2.rightHandTransform = AffineTransform.getRotateInstance(-this.ah2)
      .concatenate(AffineTransform.getScaleInstance(scale,scale));
  }
  if (this.h2 == Movement.LEFTHAND || this.h2 == Movement.GRIPLEFT) {
    if (!this.d2.leftHandVisibility) {
      this.d2.lefthand.setAttribute('visibility','visible');
      this.d2.leftHandVisibility = true;
    }
    this.d2.leftHandNewVisibility = true;
    this.d2.leftHandTransform = AffineTransform.getRotateInstance(-this.ah2)
      .concatenate(AffineTransform.getScaleInstance(scale,scale));
  }
}
////////////////////////////////////////////////////////////////////////////////
//  Dancer class
function Dancer(tamsvg,sex,x,y,angle,color,p)
{
  if (tamsvg instanceof Dancer) {
    var d = tamsvg;
    var props = ['tamsvg','fillcolor','drawcolor','startx','starty','startangle','gender'];
    for (var i in props)
      this[props[i]] = d[props[i]];
    this.path = new Path(d.path);
    if (sex)
      this.gender = sex;
    if (x || y) {
      this.startx = x;
      this.starty = -y;
    }
    this.startangle += angle;
    if (color) {
      this.fillcolor = color;
      this.drawcolor = color.darker();
    }
  } else {
    this.tamsvg = tamsvg;
    this.fillcolor = color;
    this.drawcolor = color.darker();
    this.startx = x;
    this.starty = -y;
    this.startangle = angle-90;
    this.path = new Path(p);
    this.gender = sex;
  }
  this.hidden = false;
  this.showpath = false;
  this.leftgrip = null;
  this.rightgrip = null;
  this.rightHandVisibility = false;
  this.leftHandVisibility = false;
  this.rightHandTransform = new AffineTransform();
  this.leftHandTransform = new AffineTransform();
  this.prevangle = 0;
  //  Create SVG representation
  this.svg = this.tamsvg.svg.group(this.tamsvg.dancegroup);
  //  handholds
  this.lefthand = this.tamsvg.svg.group(this.tamsvg.handholds,{visibility:'hidden'});
  this.tamsvg.svg.circle(this.lefthand,0,1,1/8,{fill:Color.orange});
  this.tamsvg.svg.line(this.lefthand,0,0,0,1,{stroke:Color.orange,'stroke-width':0.05});
  this.righthand = this.tamsvg.svg.group(this.tamsvg.handholds,{visibility:'hidden'});
  this.tamsvg.svg.circle(this.righthand,0,-1,1/8,{fill:Color.orange});
  this.tamsvg.svg.line(this.righthand,0,0,0,-1,{stroke:Color.orange,'stroke-width':0.05});
  //  body
  this.tamsvg.svg.circle(this.svg,.5,0,1/3,{fill:this.drawcolor.toString()});
  if (this.gender == Dancer.BOY)
    this.tamsvg.svg.rect(this.svg,-.5,-.5,1,1,
             {fill:this.fillcolor.toString(),
              stroke:this.drawcolor.toString(),'stroke-width':0.1});
  if (this.gender == Dancer.GIRL)
    this.tamsvg.svg.circle(this.svg,0,0,.5,
               {fill:this.fillcolor.toString(),
                stroke:this.drawcolor.toString(),'stroke-width':0.1});
  if (this.gender == Dancer.PHANTOM)
    this.tamsvg.svg.rect(this.svg,-.5,-.5,1,1,.2,.2,      // with rounded corners
             {fill:this.fillcolor.toString(),
              stroke:this.drawcolor.toString(),'stroke-width':0.1});
  this.computeStart();
  this.path.paint(this.tamsvg,this.start,this.drawcolor);
}
Dancer.BOY = 1;
Dancer.GIRL = 2;
Dancer.PHANTOM = 3;
Dancer.genders =
  { 'boy':Dancer.BOY, 'girl':Dancer.GIRL, 'phantom':Dancer.PHANTOM };

Dancer.prototype.hide = function()
{
  this.hidden = true;
  this.svg.setAttribute('visibility','hidden');
  this.lefthand.setAttribute('visibility','hidden');
  this.righthand.setAttribute('visibility','hidden');
}

Dancer.prototype.show = function()
{
  this.hidden = false;
  this.svg.setAttribute('visibility','visible');
  if (this.leftHandVisibility)
    this.lefthand.setAttribute('visibility','visible');
  if (this.rightHandVisibility)
    this.righthand.setAttribute('visibility','visible');
}

Dancer.prototype.computeStart = function()
{
  this.start = new AffineTransform();
  this.start.translate(this.startx,this.starty);
  this.start.rotate(Math.toRadians(this.startangle));
  this.svg.setAttribute('transform',this.start.toString());
}

Dancer.prototype.beats = function()
{
  var b = 0;
  if (this.path != null) {
    for (var i in this.path.movelist)
      b += this.path.movelist[i].beats;
  }
  return b;
}

Dancer.prototype.recalculate = function()
{
  this.path.recalculate();
}

//  Return distance from center
Dancer.prototype.distance = function()
{
  var x = this.tx.getTranslateX();
  var y = this.tx.getTranslateY();
  return Math.sqrt(x*x+y*y);
}

Dancer.prototype.concatenate = function(tx2)
{
  this.tx.preConcatenate(tx2);
  this.svg.setAttribute('transform',tx2.toString());
}

Dancer.prototype.paintPath = function()
{
  this.path.paint(this.tamsvg,this.start,this.drawcolor);
}

//  Compute and apply the transform for a specific time
var count = 0;
Dancer.prototype.animate = function(beat)
{
  // Be sure to reset grips at start
  if (beat == 0)
    this.rightgrip = this.leftgrip = null;
  //  Start to build transform
  //  Apply all completed movements
  this.tx = new AffineTransform(this.start);
  var m = null;
  var retval = 0;
  if (this.path != null) {
    for (var i=0; i<this.path.movelist.length; i++) {
      m = this.path.movelist[i];
      retval = 1;
      if (beat >= this.path.movelist[i].beats) {
        this.tx = new AffineTransform(this.start);
        this.tx.concatenate(this.path.transformlist[i]);
        beat -= this.path.movelist[i].beats;
        m = null;
        retval = 0;
      } else
        break;
    }
  }
  //  Apply movement in progress
  if (m != null) {
    this.tx.concatenate(m.translate(beat));
    this.tx.concatenate(m.rotate(beat));
    if (beat < 0)
      this.hands = Movement.BOTHHANDS;
    else
      this.hands = m.usehands;
    if ((m.usehands & Movement.GRIPLEFT) == 0)
      this.leftgrip = null;
    if ((m.usehands & Movement.GRIPRIGHT) == 0)
      this.rightgrip = null;
  }
  else  // End of movement
    this.hands = Movement.BOTHHANDS;  // hold hands in ending formation
}

//  Shift paths from square to hexagon
Dancer.prototype.hexagonify = function()
{
  var beat = 0;
  for (var i=0; i<this.path.movelist.length; i++) {
    var m = this.path.movelist[i];
    //  See if this movement goes clockwise or ccw around the center
    this.animate(beat);
    var vstart = new Vector(this.tx.getTranslateX(),this.tx.getTranslateY());
    this.animate(beat+m.beats/2);
    var vhalf = new Vector(this.tx.getTranslateX(),this.tx.getTranslateY());
    this.animate(beat+m.beats);
    var vend = new Vector(this.tx.getTranslateX(),this.tx.getTranslateY());
    //  See if we're moving to the left or right of the center
    var x = vend.subtract(vstart).isCW(new Vector().subtract(vstart));
    //  Now check against moving from start to 1/2
    //  and 1/2 to end
    var x2 = vhalf.subtract(vstart).isCW(new Vector().subtract(vstart));
    var x3 = vend.subtract(vhalf).isCW(new Vector().subtract(vhalf));
    //  Direction to rotate is given by x unless both
    //  x2 and x3 are different
    if (x != x2 && x != x3)
      x = x2;
    //  Get angle of start and end positions
    var a1 = vstart.angle();
    var a2 = vend.angle();
    var da = x ? a1 - a2 : a2 - a1;
    if (da < 0)
      da += Math.PI*2;
    //  Convert from square to hex by shrinking angle by 1/3
    alert("Angle before conversion: "+da*180/Math.PI+
          "\nAngle after conversion: "+(da*2/3)*180/Math.PI);
    da = da * 2 / 3;
    var tda = AffineTransform.getRotateInstance(da);
    //  Apply the rotation to the end points of the movement
    var vend2 = vend.preConcatenate(tda);
    //  Un-apply the start position to get the new Bezier point
    this.animate(beat);
    vend2 = vend2.preConcatenate(this.tx);
    //alert(vend+"\n"+vend2);

    //alert(i+"\n"+vstart+"\n"+vhalf+"\n"+vend+"\n"+
    //    x + " " + x2 + " " + x3 + "\n"+
    //    Math.round(180*da/Math.PI)); // + " " + Math.round(180*a2/Math.PI));



    beat += this.path.movelist[i].beats;
  }
  this.recalculate();
}

Dancer.prototype.paint = function()
{
  this.svg.setAttribute('transform',this.tx.toString());
  this.righthand.setAttribute('transform',
      new AffineTransform(this.tx).concatenate(this.rightHandTransform).toString());
  this.lefthand.setAttribute('transform',
      new AffineTransform(this.tx).concatenate(this.leftHandTransform).toString());
  return retval;
}

////////////////////////////////////////////////////////////////////////////////
//  Path class
function Path(p)
{
  this.movelist = [];
  this.transformlist = [];
  this.pathlist = [];
  if (p instanceof Path) {
    for (var m in p.movelist)
      this.add(p.movelist[m].clone());
  }
  else if (p) {
    for (var i=0; i<p.length; i++) {
      var m = p[i].cx3 == undefined
          ? new Movement(p[i].hands,
                         p[i].beats,
                         p[i].cx1,p[i].cy1,
                         p[i].cx2,p[i].cy2,
                         p[i].x2,p[i].y2)
          : new Movement(p[i].hands,
                         p[i].beats,
                         p[i].cx1,p[i].cy1,
                         p[i].cx2,p[i].cy2,
                         p[i].x2,p[i].y2,
                         p[i].cx3,0,   // cy3 is always 0
                         p[i].cx4,p[i].cy4,
                         p[i].x4,p[i].y4);
      this.add(m);
    }
  }
}

Path.prototype.recalculate = function()
{
  this.transformlist = [];
  var tx = new AffineTransform();
  for (var i in this.movelist) {
    tx.concatenate(this.movelist[i].translate(999));
    tx.concatenate(this.movelist[i].rotate(999));
    this.transformlist.push(new AffineTransform(tx));
  }
}
//  Return total number of beats in path
Path.prototype.beats = function()
{
  var b = 0.0;
  if (this.movelist != null) {
    for (var i in this.movelist)
      b += this.movelist[i].beats;
  }
  return b;
}

//  Make the path run slower or faster to complete in a given number of beats
Path.prototype.changebeats = function(newbeats)
{
  if (this.movelist != null) {
    var factor = newbeats/this.beats();
    for (var i in this.movelist)
      this.movelist[i].beats *= factor;
  }
}

//  Change hand usage
Path.prototype.changehands = function(hands)
{
  if (this.movelist != null) {
    for (var i in this.movelist)
      this.movelist[i].useHands(hands);
  }
}

//  Change the path by scale factors
Path.prototype.scale = function(x,y)
{
  if (this.movelist != null) {
    for (var i in this.movelist)
      this.movelist[i].scale(x,y);
  }
}

//  Skew the path by translating the destination point
Path.prototype.skew = function(x,y)
{
  if (self.movelist != null) {
    for (var i in this.movelist)
      this.movelist[i].skew(x,y);
  }
}

//  Append one movement to the end of the Path
Path.prototype.add = function(m)
{
  if (m instanceof Movement)
    this.movelist.push(m);
  if (m instanceof Path)
    this.movelist = this.movelist.concat(m.movelist);
  this.recalculate();
  return this;
}

//  Reflect the path about the x-axis
Path.prototype.reflect = function()
{
  for (var i in this.movelist)
    this.movelist[i].reflect();
  this.recalculate();
  return this;
}


  // Draw the curves that make up the path
Path.prototype.paint = function(tamsvg, /*AffineTransform*/ start, color)
{
  this.pathlist = [];
  var t1 = new AffineTransform();
  //t1.scale(1,-1);
  //t1.rotate(Math.toRadians(180));
  t1.concatenate(start);
  for (var i in this.movelist) {
    var m = this.movelist[i];
    var p = tamsvg.svg.createPath();
    var mb = m.btranslate;
    tamsvg.svg.path(tamsvg.pathgroup,
        p.moveTo(0,0).curveCTo(mb.ctrlx1,mb.ctrly1,mb.ctrlx2,mb.ctrly2,mb.x2,mb.y2),
        {fill:'none',stroke:color.toString(),strokeWidth:0.1,strokeOpacity:.3,
         transform:t1.toString()});
    //g2.draw(t1.createTransformedShape(m.btranslate));
    //tamsvg.svg.circle(tamsvg.pathgroup,0,1,1,{fill:Color.green});
    t1 = new AffineTransform();  // (start)
    //t1.scale(1,-1);
    //t1.rotate(Math.toRadians(180));
    t1.concatenate(start);
    t1.concatenate(this.transformlist[i]);
  }
}

////////////////////////////////////////////////////////////////////////////////
//  Movement class
//  Constructor for independent heading and movement
function Movement(h,b,ctrlx1,ctrly1,ctrlx2,ctrly2,x2,y2,
                    ctrlx3,ctrly3,ctrlx4,ctrly4,x4,y4)
{
  this.btranslate = new Bezier(0,0,ctrlx1,ctrly1,ctrlx2,ctrly2,x2,y2);
  if (arguments.length > 8)
    this.brotate = new Bezier(0,0,ctrlx3,ctrly3,ctrlx4,ctrly4,x4,y4);
  else
    this.brotate = new Bezier(0,0,ctrlx1,ctrly1,ctrlx2,ctrly2,x2,y2);
  this.beats = b;
  this.usehands = Movement.setHands[h];
}
Movement.NOHANDS = 0;
Movement.LEFTHAND = 1;
Movement.RIGHTHAND = 2;
Movement.BOTHHANDS = 3;
Movement.GRIPLEFT = 5;
Movement.GRIPRIGHT = 6;
Movement.GRIPBOTH = 7;
Movement.ANYGRIP = 4;

Movement.setHands = { "none": Movement.NOHANDS,
                      "left": Movement.LEFTHAND,
                      "right": Movement.RIGHTHAND,
                      "both": Movement.BOTHHANDS,
                      "gripleft": Movement.GRIPLEFT,
                      "gripright": Movement.GRIPRIGHT,
                      "gripboth": Movement.GRIPBOTH,
                      "anygrip": Movement.ANYGRIP };

Movement.prototype.useHands = function(h)
{
  this.usehands = h;
  return this;
}

Movement.prototype.clone = function()
{
  var m = new Movement(this.usehands,
                       this.beats,
                       this.btranslate.ctrlx1,this.btranslate.ctrly1,
                       this.btranslate.ctrlx2,this.btranslate.ctrly2,
                       this.btranslate.x2,this.btranslate.y2,
                       this.brotate.ctrlx1,this.brotate.ctrly1,
                       this.brotate.ctrlx2,this.brotate.ctrly2,
                       this.brotate.x2,this.brotate.y2);
  return m;
}

Movement.prototype.translate = function(t) {
  t = Math.min(Math.max(0,t),this.beats);
  return this.btranslate.translate(t/this.beats);
}

Movement.prototype.reflect = function()
{
  return this.scale(1,-1);
}

Movement.prototype.rotate = function(t)
{
  t = Math.min(Math.max(0,t),this.beats);
  return this.brotate.rotate(t/this.beats);
}

Movement.prototype.scale = function(x,y)
{
  this.btranslate = new Bezier(0,0,this.btranslate.ctrlx1*x,
                                   this.btranslate.ctrly1*y,
                                   this.btranslate.ctrlx2*x,
                                   this.btranslate.ctrly2*y,
                                   this.btranslate.x2*x,
                                   this.btranslate.y2*y);
  this.brotate = new Bezier(0,0,this.brotate.ctrlx1*x,
                                this.brotate.ctrly1*y,
                                this.brotate.ctrlx2*x,
                                this.brotate.ctrly2*y,
                                this.brotate.x2*x,
                                this.brotate.y2*y);
  if (y < 0) {
    if (this.usehands == Movement.LEFTHAND)
      this.usehands = Movement.RIGHTHAND;
    else if (this.usehands == Movement.RIGHTHAND)
      this.usehands = Movement.LEFTHAND;
  }
  return this;
}

//  Skew the movement by translating the destination point
Movement.prototype.skew = function(x,y)
{
  this.btranslate = new Bezier(0,0,this.btranslate.ctrlx1,
                                   this.btranslate.ctrly1,
                                   this.btranslate.ctrlx2+x,
                                   this.btranslate.ctrly2+y,
                                   this.btranslate.x2+x,
                                   this.btranslate.y2+y);
  return this;
}

////////////////////////////////////////////////////////////////////////////////
//  Bezier class
function Bezier(x1,y1,ctrlx1,ctrly1,ctrlx2,ctrly2,x2,y2)
{
  this.x1 = x1;
  this.y1 = y1;
  this.ctrlx1 = ctrlx1;
  this.ctrly1 = ctrly1;
  this.ctrlx2 = ctrlx2;
  this.ctrly2 = ctrly2;
  this.x2 = x2;
  this.y2 = y2;
  this.calculatecoefficients();
}

Bezier.prototype.calculatecoefficients = function()
{
  this.cx = 3.0*(this.ctrlx1-this.x1);
  this.bx = 3.0*(this.ctrlx2-this.ctrlx1) - this.cx;
  this.ax = this.x2 - this.x1 - this.cx - this.bx;

  this.cy = 3.0*(this.ctrly1-this.y1);
  this.by = 3.0*(this.ctrly2-this.ctrly1) - this.cy;
  this.ay = this.y2 - this.y1 - this.cy - this.by;
}
//  Return the movement along the curve given "t" between 0 and 1
Bezier.prototype.translate = function(t)
{
  var x = this.x1 + t*(this.cx + t*(this.bx + t*this.ax));
  var y = this.y1 + t*(this.cy + t*(this.by + t*this.ay));
  return AffineTransform.getTranslateInstance(x,y);
}

//  Return the angle of the derivative given "t" between 0 and 1
Bezier.prototype.rotate = function(t)
{
  var x = this.cx + t*(2.0*this.bx + t*3.0*this.ax);
  var y = this.cy + t*(2.0*this.by + t*3.0*this.ay);
  var theta = Math.atan2(y,x);
  return AffineTransform.getRotateInstance(theta);
}

////////////////////////////////////////////////////////////////////////////////
//   Vector class
//  constructor
function Vector(x,y,z)
{
  if (arguments.length > 0 && x instanceof Vector) {
    this.x = x.x;
    this.y = x.y;
    this.z = x.z;
  } else {
    this.x = arguments.length > 0 ? x : 0;
    this.y = arguments.length > 1 ? y : 0;
    this.z = arguments.length > 2 ? z : 0;
  }
}
//  Add/subtract two vectors
Vector.prototype.add = function(v)
{
  return new Vector(thix.x+v.x,this.y+v.y,this.z+v.z);
}
Vector.prototype.subtract = function(v)
{
  return new Vector(this.x-v.x,this.y-v.y,this.z-v.z);
}
//  Compute the cross product
Vector.prototype.cross = function(v)
{
  return new Vector(
    this.y*v.z - this.z*v.y,
    this.z*v.x - this.x*v.z,
    this.x*v.y - this.y*v.x
  );
}

//  Return angle of vector from the origin
Vector.prototype.angle = function()
{
  return Math.atan2(this.y,this.x);
}
//  Rotate by a given angle
Vector.prototype.rotate = function(th)
{
  var d = Math.sqrt(this.x*this.x+this.y*this.y);
  var a = this.angle() + th;
  return new Vector(
      d * Math.cos(a),
      d * Math.sin(a),
      this.z);
}
//  Apply a transform
Vector.prototype.concatenate = function(tx)
{
  var vx = AffineTransform.getTranslateInstance(this.x,this.y);
  vx = vx.concatenate(tx);
  return new Vector(vx.getTranslateX(),vx.getTranslateY());
}
Vector.prototype.preConcatenate = function(tx)
{
  var vx = AffineTransform.getTranslateInstance(this.x,this.y);
  vx = vx.preConcatenate(tx);
  return new Vector(vx.getTranslateX(),vx.getTranslateY());
}

//  Return true if this vector followed by vector 2 is clockwise
Vector.prototype.isCW = function(v)
{
  return this.cross(v).z < 0;
}
Vector.prototype.isCCW = function(v)
{
  return this.cross(v).z > 0;
}
Vector.prototype.toString = function()
{
  return "("+Math.round(this.x*10)/10+","+Math.round(this.y*10)/10+")";
}
//  Return true if the vector from this to v points left of the origin
Vector.prototype.isLeft = function(v)
{
  var v1 = new Vector().subtract(this);
  var v2 = new Vector(v).subtract(this);
  return v1.isCCW(v2);
}
////////////////////////////////////////////////////////////////////////////////
//   AffineTransform class
//   constructor
function AffineTransform(tx)
{
  if (arguments.length == 0) {
    //  default constructor - return the identity matrix
    this.x1 = 1.0;
    this.x2 = 0.0;
    this.x3 = 0.0;
    this.y1 = 0.0;
    this.y2 = 1.0;
    this.y3 = 0.0;
  }
  else if (tx instanceof AffineTransform) {
    //  return a copy
    this.x1 = tx.x1;
    this.x2 = tx.x2;
    this.x3 = tx.x3;
    this.y1 = tx.y1;
    this.y2 = tx.y2;
    this.y3 = tx.y3;
  }
}
//  Generate a new transform that moves to a new location
AffineTransform.getTranslateInstance = function(x,y)
{
  a = new AffineTransform();
  a.x3 = x;
  a.y3 = y;
  return a;
}
//  Generate a new transform that does a rotation
AffineTransform.getRotateInstance = function(theta)
{
  a = new AffineTransform();
  a.x1 = a.y2 = Math.cos(theta);
  a.x2 = -(a.y1 = Math.sin(theta));
  return a;
}

AffineTransform.getScaleInstance = function(x,y)
{
  a = new AffineTransform();
  a.scale(x,y);
  return a;
}

//  Add a translation to this transform
AffineTransform.prototype.translate = function(x,y)
{
  this.x3 += x*this.x1 + y*this.x2;
  this.y3 += x*this.y1 + y*this.y2;
}

//  Add a scaling to this transform
AffineTransform.prototype.scale = function(x,y)
{
  this.x1 *= x;
  this.y1 *= x;
  this.x2 *= y;
  this.y2 *= y;
}

//  Add a rotation to this transform
AffineTransform.prototype.rotate = function(angle)
{
  var sin = Math.sin(angle);
  var cos = Math.cos(angle);
  var copy = { x1: this.x1, x2: this.x2, y1: this.y1, y2: this.y2 };
  this.x1 =  cos * copy.x1 + sin * copy.x2;
  this.x2 = -sin * copy.x1 + cos * copy.x2;
  this.y1 =  cos * copy.y1 + sin * copy.y2;
  this.y2 = -sin * copy.y1 + cos * copy.y2;
  return this;
}

AffineTransform.prototype.concatenate = function(tx)
{
  // [this] = [this] x [Tx]
  var copy = { x1: this.x1, x2: this.x2, x3: this.x3,
               y1: this.y1, y2: this.y2, y3: this.y3 };
  this.x1 = copy.x1 * tx.x1 + copy.x2 * tx.y1;
  this.x2 = copy.x1 * tx.x2 + copy.x2 * tx.y2;
  this.x3 = copy.x1 * tx.x3 + copy.x2 * tx.y3 + copy.x3;
  this.y1 = copy.y1 * tx.x1 + copy.y2 * tx.y1;
  this.y2 = copy.y1 * tx.x2 + copy.y2 * tx.y2;
  this.y3 = copy.y1 * tx.x3 + copy.y2 * tx.y3 + copy.y3;
  return this;
}

AffineTransform.prototype.preConcatenate = function(tx)
{
  // [this] = [Tx] x [this]
  var copy = { x1: this.x1, x2: this.x2, x3: this.x3,
               y1: this.y1, y2: this.y2, y3: this.y3 };
  this.x1 = tx.x1 * copy.x1 + tx.x2 * copy.y1;
  this.x2 = tx.x1 * copy.x2 + tx.x2 * copy.y2;
  this.x3 = tx.x1 * copy.x3 + tx.x2 * copy.y3 + tx.x3;
  this.y1 = tx.y1 * copy.x1 + tx.y2 * copy.y1;
  this.y2 = tx.y1 * copy.x2 + tx.y2 * copy.y2;
  this.y3 = tx.y1 * copy.x3 + tx.y2 * copy.y3 + tx.y3;
  return this;
}
AffineTransform.prototype.getScaleX = function()
{
  return this.x1;
}
AffineTransform.prototype.getScaleY = function()
{
  return this.y2;
}
AffineTransform.prototype.getShearX = function()
{
  return this.x2;
}
AffineTransform.prototype.getShearY = function()
{
  return this.y1;
}
AffineTransform.prototype.getTranslateX = function()
{
  return this.x3;
}
AffineTransform.prototype.getTranslateY = function()
{
  return this.y3;
}

//  Return a string that can be used as the svg transform attribute
AffineTransform.prototype.toString = function()
{
  return 'matrix('+this.x1+','+this.y1+','+
                   this.x2+','+this.y2+','+
                   this.x3+','+this.y3+')';
}
////////////////////////////////////////////////////////////////////////////////
//   Color class
function Color(r,g,b)
{
  this.r = Math.floor(r);
  this.g = Math.floor(g);
  this.b = Math.floor(b);
}
Color.FACTOR = 0.7;  // from Java
Color.hex = [ '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'];
Color.black = new Color(0,0,0);
Color.red = new Color(255,0,0);
Color.green = new Color(0,255,0);
Color.blue = new Color(0,0,255);
Color.yellow = new Color(255,255,0);
Color.orange = new Color(255,200,0);
Color.lightGray = new Color(192,192,192);
Color.gray = new Color(128,128,128);
Color.magenta = new Color(255,0,255);
Color.cyan = new Color(0,255,255);
Color.prototype.invert = function()
{
  return new Color(255-this.r,255-this.g,255-this.b);
}
Color.prototype.darker = function()
{
  return new Color( Math.floor(this.r*Color.FACTOR),
                    Math.floor(this.g*Color.FACTOR),
                    Math.floor(this.b*Color.FACTOR));
}
Color.prototype.brighter = function()
{
  return this.invert().darker().invert();
}
Color.prototype.rotate = function()
{
  var c = new Color(0,0,0);
  if (this.r == 255 && this.g == 0 && this.b == 0)
    c.g = c.b = 255;
  else if (this.r == Color.lightGray.r)
    c = Color.lightGray;
  else
    c.b = 255;
  return c;
}

Color.prototype.toString = function()
{
  return '#' + Color.hex[this.r>>4] + Color.hex[this.r&0xf] +
               Color.hex[this.g>>4] + Color.hex[this.g&0xf] +
               Color.hex[this.b>>4] + Color.hex[this.b&0xf];
}
////////////////////////////////////////////////////////////////////////////////
//  Misc
Math.toRadians = function(deg)
{
  return deg * Math.PI / 180;
}
Math.IEEEremainder = function(d1,d2)
{
  var n = Math.round(d1/d2);
  return d1 - n*d2;
}
