// Copyright 2006 Google Inc. // // Licensed under the Apache License, Version 2.0 (the “License”); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an “AS IS” BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.

// Known Issues: // // * Patterns are not implemented. // * Radial gradient are not implemented. The VML version of these look very // different from the canvas one. // * Clipping paths are not implemented. // * Coordsize. The width and height attribute have higher priority than the // width and height style values which isn't correct. // * Painting mode isn't implemented. // * Canvas width/height should is using content-box by default. IE in // Quirks mode will draw the canvas using border-box. Either change your // doctype to HTML5 // (www.whatwg.org/specs/web-apps/current-work/#the-doctype) // or use Box Sizing Behavior from WebFX // (webfx.eae.net/dhtml/boxsizing/boxsizing.html) // * Non uniform scaling does not correctly scale strokes. // * Optimize. There is always room for speed improvements.

// Only add this code if we do not already have a canvas implementation if (!document.createElement('canvas').getContext) {

(function() {

// alias some functions to make (compiled) code shorter
var m = Math;
var mr = m.round;
var ms = m.sin;
var mc = m.cos;
var abs = m.abs;
var sqrt = m.sqrt;

// this is used for sub pixel precision
var Z = 10;
var Z2 = Z / 2;

/**
 * This funtion is assigned to the <canvas> elements as element.getContext().
 * @this {HTMLElement}
 * @return {CanvasRenderingContext2D_}
 */
function getContext() {
  return this.context_ ||
      (this.context_ = new CanvasRenderingContext2D_(this));
}

var slice = Array.prototype.slice;

/**
 * Binds a function to an object. The returned function will always use the
 * passed in {@code obj} as {@code this}.
 *
 * Example:
 *
 *   g = bind(f, obj, a, b)
 *   g(c, d) // will do f.call(obj, a, b, c, d)
 *
 * @param {Function} f The function to bind the object to
 * @param {Object} obj The object that should act as this when the function
 *     is called
 * @param {*} var_args Rest arguments that will be used as the initial
 *     arguments when the function is called
 * @return {Function} A new function that has bound this
 */
function bind(f, obj, var_args) {
  var a = slice.call(arguments, 2);
  return function() {
    return f.apply(obj, a.concat(slice.call(arguments)));
  };
}

var G_vmlCanvasManager_ = {
  init: function(opt_doc) {
    if (/MSIE/.test(navigator.userAgent) && !window.opera) {
      var doc = opt_doc || document;
      // Create a dummy element so that IE will allow canvas elements to be
      // recognized.
      doc.createElement('canvas');
      doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
    }
  },

  init_: function(doc) {
    // create xmlns
    if (!doc.namespaces['g_vml_']) {
      doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
                         '#default#VML');

    }
    if (!doc.namespaces['g_o_']) {
      doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
                         '#default#VML');
    }

    // Setup default CSS.  Only add one style sheet per document
    if (!doc.styleSheets['ex_canvas_']) {
      var ss = doc.createStyleSheet();
      ss.owningElement.id = 'ex_canvas_';
      ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
          // default size is 300x150 in Gecko and Opera
          'text-align:left;width:300px;height:150px}' +
          'g_vml_\\:*{behavior:url(#default#VML)}' +
          'g_o_\\:*{behavior:url(#default#VML)}';

    }

    // find all canvas elements
    var els = doc.getElementsByTagName('canvas');
    for (var i = 0; i < els.length; i++) {
      this.initElement(els[i]);
    }
  },

  /**
   * Public initializes a canvas element so that it can be used as canvas
   * element from now on. This is called automatically before the page is
   * loaded but if you are creating elements using createElement you need to
   * make sure this is called on the element.
   * @param {HTMLElement} el The canvas element to initialize.
   * @return {HTMLElement} the element that was created.
   */
  initElement: function(el) {
    if (!el.getContext) {

      el.getContext = getContext;

      // Remove fallback content. There is no way to hide text nodes so we
      // just remove all childNodes. We could hide all elements and remove
      // text nodes but who really cares about the fallback content.
      el.innerHTML = '';

      // do not use inline function because that will leak memory
      el.attachEvent('onpropertychange', onPropertyChange);
      el.attachEvent('onresize', onResize);

      var attrs = el.attributes;
      if (attrs.width && attrs.width.specified) {
        // TODO: use runtimeStyle and coordsize
        // el.getContext().setWidth_(attrs.width.nodeValue);
        el.style.width = attrs.width.nodeValue + 'px';
      } else {
        el.width = el.clientWidth;
      }
      if (attrs.height && attrs.height.specified) {
        // TODO: use runtimeStyle and coordsize
        // el.getContext().setHeight_(attrs.height.nodeValue);
        el.style.height = attrs.height.nodeValue + 'px';
      } else {
        el.height = el.clientHeight;
      }
      //el.getContext().setCoordsize_()
    }
    return el;
  }
};

function onPropertyChange(e) {
  var el = e.srcElement;

  switch (e.propertyName) {
    case 'width':
      el.style.width = el.attributes.width.nodeValue + 'px';
      el.getContext().clearRect();
      break;
    case 'height':
      el.style.height = el.attributes.height.nodeValue + 'px';
      el.getContext().clearRect();
      break;
  }
}

function onResize(e) {
  var el = e.srcElement;
  if (el.firstChild) {
    el.firstChild.style.width =  el.clientWidth + 'px';
    el.firstChild.style.height = el.clientHeight + 'px';
  }
}

G_vmlCanvasManager_.init();

// precompute "00" to "FF"
var dec2hex = [];
for (var i = 0; i < 16; i++) {
  for (var j = 0; j < 16; j++) {
    dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
  }
}

function createMatrixIdentity() {
  return [
    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1]
  ];
}

function matrixMultiply(m1, m2) {
  var result = createMatrixIdentity();

  for (var x = 0; x < 3; x++) {
    for (var y = 0; y < 3; y++) {
      var sum = 0;

      for (var z = 0; z < 3; z++) {
        sum += m1[x][z] * m2[z][y];
      }

      result[x][y] = sum;
    }
  }
  return result;
}

function copyState(o1, o2) {
  o2.fillStyle     = o1.fillStyle;
  o2.lineCap       = o1.lineCap;
  o2.lineJoin      = o1.lineJoin;
  o2.lineWidth     = o1.lineWidth;
  o2.miterLimit    = o1.miterLimit;
  o2.shadowBlur    = o1.shadowBlur;
  o2.shadowColor   = o1.shadowColor;
  o2.shadowOffsetX = o1.shadowOffsetX;
  o2.shadowOffsetY = o1.shadowOffsetY;
  o2.strokeStyle   = o1.strokeStyle;
  o2.globalAlpha   = o1.globalAlpha;
  o2.arcScaleX_    = o1.arcScaleX_;
  o2.arcScaleY_    = o1.arcScaleY_;
  o2.lineScale_    = o1.lineScale_;
}

function processStyle(styleString) {
  var str, alpha = 1;

  styleString = String(styleString);
  if (styleString.substring(0, 3) == 'rgb') {
    var start = styleString.indexOf('(', 3);
    var end = styleString.indexOf(')', start + 1);
    var guts = styleString.substring(start + 1, end).split(',');

    str = '#';
    for (var i = 0; i < 3; i++) {
      str += dec2hex[Number(guts[i])];
    }

    if (guts.length == 4 && styleString.substr(3, 1) == 'a') {
      alpha = guts[3];
    }
  } else {
    str = styleString;
  }

  return {color: str, alpha: alpha};
}

function processLineCap(lineCap) {
  switch (lineCap) {
    case 'butt':
      return 'flat';
    case 'round':
      return 'round';
    case 'square':
    default:
      return 'square';
  }
}

/**
 * This class implements CanvasRenderingContext2D interface as described by
 * the WHATWG.
 * @param {HTMLElement} surfaceElement The element that the 2D context should
 * be associated with
 */
function CanvasRenderingContext2D_(surfaceElement) {
  this.m_ = createMatrixIdentity();

  this.mStack_ = [];
  this.aStack_ = [];
  this.currentPath_ = [];

  // Canvas context properties
  this.strokeStyle = '#000';
  this.fillStyle = '#000';

  this.lineWidth = 1;
  this.lineJoin = 'miter';
  this.lineCap = 'butt';
  this.miterLimit = Z * 1;
  this.globalAlpha = 1;
  this.canvas = surfaceElement;

  var el = surfaceElement.ownerDocument.createElement('div');
  el.style.width =  surfaceElement.clientWidth + 'px';
  el.style.height = surfaceElement.clientHeight + 'px';
  el.style.overflow = 'hidden';
  el.style.position = 'absolute';
  surfaceElement.appendChild(el);

  this.element_ = el;
  this.arcScaleX_ = 1;
  this.arcScaleY_ = 1;
  this.lineScale_ = 1;
}

var contextPrototype = CanvasRenderingContext2D_.prototype;
contextPrototype.clearRect = function() {
  this.element_.innerHTML = '';
};

contextPrototype.beginPath = function() {
  // TODO: Branch current matrix so that save/restore has no effect
  //       as per safari docs.
  this.currentPath_ = [];
};

contextPrototype.moveTo = function(aX, aY) {
  var p = this.getCoords_(aX, aY);
  this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});
  this.currentX_ = p.x;
  this.currentY_ = p.y;
};

contextPrototype.lineTo = function(aX, aY) {
  var p = this.getCoords_(aX, aY);
  this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});

  this.currentX_ = p.x;
  this.currentY_ = p.y;
};

contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
                                          aCP2x, aCP2y,
                                          aX, aY) {
  var p = this.getCoords_(aX, aY);
  var cp1 = this.getCoords_(aCP1x, aCP1y);
  var cp2 = this.getCoords_(aCP2x, aCP2y);
  bezierCurveTo(this, cp1, cp2, p);
};

// Helper function that takes the already fixed cordinates.
function bezierCurveTo(self, cp1, cp2, p) {
  self.currentPath_.push({
    type: 'bezierCurveTo',
    cp1x: cp1.x,
    cp1y: cp1.y,
    cp2x: cp2.x,
    cp2y: cp2.y,
    x: p.x,
    y: p.y
  });
  self.currentX_ = p.x;
  self.currentY_ = p.y;
}

contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
  // the following is lifted almost directly from
  // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes

  var cp = this.getCoords_(aCPx, aCPy);
  var p = this.getCoords_(aX, aY);

  var cp1 = {
    x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),
    y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)
  };
  var cp2 = {
    x: cp1.x + (p.x - this.currentX_) / 3.0,
    y: cp1.y + (p.y - this.currentY_) / 3.0
  };

  bezierCurveTo(this, cp1, cp2, p);
};

contextPrototype.arc = function(aX, aY, aRadius,
                                aStartAngle, aEndAngle, aClockwise) {
  aRadius *= Z;
  var arcType = aClockwise ? 'at' : 'wa';

  var xStart = aX + mc(aStartAngle) * aRadius - Z2;
  var yStart = aY + ms(aStartAngle) * aRadius - Z2;

  var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
  var yEnd = aY + ms(aEndAngle) * aRadius - Z2;

  // IE won't render arches drawn counter clockwise if xStart == xEnd.
  if (xStart == xEnd && !aClockwise) {
    xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
                     // that can be represented in binary
  }

  var p = this.getCoords_(aX, aY);
  var pStart = this.getCoords_(xStart, yStart);
  var pEnd = this.getCoords_(xEnd, yEnd);

  this.currentPath_.push({type: arcType,
                         x: p.x,
                         y: p.y,
                         radius: aRadius,
                         xStart: pStart.x,
                         yStart: pStart.y,
                         xEnd: pEnd.x,
                         yEnd: pEnd.y});

};

contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
  this.moveTo(aX, aY);
  this.lineTo(aX + aWidth, aY);
  this.lineTo(aX + aWidth, aY + aHeight);
  this.lineTo(aX, aY + aHeight);
  this.closePath();
};

contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
  var oldPath = this.currentPath_;
  this.beginPath();

  this.moveTo(aX, aY);
  this.lineTo(aX + aWidth, aY);
  this.lineTo(aX + aWidth, aY + aHeight);
  this.lineTo(aX, aY + aHeight);
  this.closePath();
  this.stroke();

  this.currentPath_ = oldPath;
};

contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
  var oldPath = this.currentPath_;
  this.beginPath();

  this.moveTo(aX, aY);
  this.lineTo(aX + aWidth, aY);
  this.lineTo(aX + aWidth, aY + aHeight);
  this.lineTo(aX, aY + aHeight);
  this.closePath();
  this.fill();

  this.currentPath_ = oldPath;
};

contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
  var gradient = new CanvasGradient_('gradient');
  gradient.x0_ = aX0;
  gradient.y0_ = aY0;
  gradient.x1_ = aX1;
  gradient.y1_ = aY1;
  return gradient;
};

contextPrototype.createRadialGradient = function(aX0, aY0, aR0,
                                                 aX1, aY1, aR1) {
  var gradient = new CanvasGradient_('gradientradial');
  gradient.x0_ = aX0;
  gradient.y0_ = aY0;
  gradient.r0_ = aR0;
  gradient.x1_ = aX1;
  gradient.y1_ = aY1;
  gradient.r1_ = aR1;
  return gradient;
};

contextPrototype.drawImage = function(image, var_args) {
  var dx, dy, dw, dh, sx, sy, sw, sh;

  // to find the original width we overide the width and height
  var oldRuntimeWidth = image.runtimeStyle.width;
  var oldRuntimeHeight = image.runtimeStyle.height;
  image.runtimeStyle.width = 'auto';
  image.runtimeStyle.height = 'auto';

  // get the original size
  var w = image.width;
  var h = image.height;

  // and remove overides
  image.runtimeStyle.width = oldRuntimeWidth;
  image.runtimeStyle.height = oldRuntimeHeight;

  if (arguments.length == 3) {
    dx = arguments[1];
    dy = arguments[2];
    sx = sy = 0;
    sw = dw = w;
    sh = dh = h;
  } else if (arguments.length == 5) {
    dx = arguments[1];
    dy = arguments[2];
    dw = arguments[3];
    dh = arguments[4];
    sx = sy = 0;
    sw = w;
    sh = h;
  } else if (arguments.length == 9) {
    sx = arguments[1];
    sy = arguments[2];
    sw = arguments[3];
    sh = arguments[4];
    dx = arguments[5];
    dy = arguments[6];
    dw = arguments[7];
    dh = arguments[8];
  } else {
    throw Error('Invalid number of arguments');
  }

  var d = this.getCoords_(dx, dy);

  var w2 = sw / 2;
  var h2 = sh / 2;

  var vmlStr = [];

  var W = 10;
  var H = 10;

  // For some reason that I've now forgotten, using divs didn't work
  vmlStr.push(' <g_vml_:group',
              ' coordsize="', Z * W, ',', Z * H, '"',
              ' coordorigin="0,0"' ,
              ' style="width:', W, 'px;height:', H, 'px;position:absolute;');

  // If filters are necessary (rotation exists), create them
  // filters are bog-slow, so only create them if abbsolutely necessary
  // The following check doesn't account for skews (which don't exist
  // in the canvas spec (yet) anyway.

  if (this.m_[0][0] != 1 || this.m_[0][1]) {
    var filter = [];

    // Note the 12/21 reversal
    filter.push('M11=', this.m_[0][0], ',',
                'M12=', this.m_[1][0], ',',
                'M21=', this.m_[0][1], ',',
                'M22=', this.m_[1][1], ',',
                'Dx=', mr(d.x / Z), ',',
                'Dy=', mr(d.y / Z), '');

    // Bounding box calculation (need to minimize displayed area so that
    // filters don't waste time on unused pixels.
    var max = d;
    var c2 = this.getCoords_(dx + dw, dy);
    var c3 = this.getCoords_(dx, dy + dh);
    var c4 = this.getCoords_(dx + dw, dy + dh);

    max.x = m.max(max.x, c2.x, c3.x, c4.x);
    max.y = m.max(max.y, c2.y, c3.y, c4.y);

    vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z),
                'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(',
                filter.join(''), ", sizingmethod='clip');")
  } else {
    vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;');
  }

  vmlStr.push(' ">' ,
              '<g_vml_:image src="', image.src, '"',
              ' style="width:', Z * dw, 'px;',
              ' height:', Z * dh, 'px;"',
              ' cropleft="', sx / w, '"',
              ' croptop="', sy / h, '"',
              ' cropright="', (w - sx - sw) / w, '"',
              ' cropbottom="', (h - sy - sh) / h, '"',
              ' />',
              '</g_vml_:group>');

  this.element_.insertAdjacentHTML('BeforeEnd',
                                  vmlStr.join(''));
};

contextPrototype.stroke = function(aFill) {
  var lineStr = [];
  var lineOpen = false;
  var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
  var color = a.color;
  var opacity = a.alpha * this.globalAlpha;

  var W = 10;
  var H = 10;

  lineStr.push('<g_vml_:shape',
               ' filled="', !!aFill, '"',
               ' style="position:absolute;width:', W, 'px;height:', H, 'px;"',
               ' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"',
               ' stroked="', !aFill, '"',
               ' path="');

  var newSeq = false;
  var min = {x: null, y: null};
  var max = {x: null, y: null};

  for (var i = 0; i < this.currentPath_.length; i++) {
    var p = this.currentPath_[i];
    var c;

    switch (p.type) {
      case 'moveTo':
        c = p;
        lineStr.push(' m ', mr(p.x), ',', mr(p.y));
        break;
      case 'lineTo':
        lineStr.push(' l ', mr(p.x), ',', mr(p.y));
        break;
      case 'close':
        lineStr.push(' x ');
        p = null;
        break;
      case 'bezierCurveTo':
        lineStr.push(' c ',
                     mr(p.cp1x), ',', mr(p.cp1y), ',',
                     mr(p.cp2x), ',', mr(p.cp2y), ',',
                     mr(p.x), ',', mr(p.y));
        break;
      case 'at':
      case 'wa':
        lineStr.push(' ', p.type, ' ',
                     mr(p.x - this.arcScaleX_ * p.radius), ',',
                     mr(p.y - this.arcScaleY_ * p.radius), ' ',
                     mr(p.x + this.arcScaleX_ * p.radius), ',',
                     mr(p.y + this.arcScaleY_ * p.radius), ' ',
                     mr(p.xStart), ',', mr(p.yStart), ' ',
                     mr(p.xEnd), ',', mr(p.yEnd));
        break;
    }

    // TODO: Following is broken for curves due to
    //       move to proper paths.

    // Figure out dimensions so we can do gradient fills
    // properly
    if (p) {
      if (min.x == null || p.x < min.x) {
        min.x = p.x;
      }
      if (max.x == null || p.x > max.x) {
        max.x = p.x;
      }
      if (min.y == null || p.y < min.y) {
        min.y = p.y;
      }
      if (max.y == null || p.y > max.y) {
        max.y = p.y;
      }
    }
  }
  lineStr.push(' ">');

  if (!aFill) {
    var lineWidth = this.lineScale_ * this.lineWidth;

    // VML cannot correctly render a line if the width is less than 1px.
    // In that case, we dilute the color to make the line look thinner.
    if (lineWidth < 1) {
      opacity *= lineWidth;
    }

    lineStr.push(
      '<g_vml_:stroke',
      ' opacity="', opacity, '"',
      ' joinstyle="', this.lineJoin, '"',
      ' miterlimit="', this.miterLimit, '"',
      ' endcap="', processLineCap(this.lineCap), '"',
      ' weight="', lineWidth, 'px"',
      ' color="', color, '" />'
    );
  } else if (typeof this.fillStyle == 'object') {
    var fillStyle = this.fillStyle;
    var angle = 0;
    var focus = {x: 0, y: 0};

    // additional offset
    var shift = 0;
    // scale factor for offset
    var expansion = 1;

    if (fillStyle.type_ == 'gradient') {
      var x0 = fillStyle.x0_ / this.arcScaleX_;
      var y0 = fillStyle.y0_ / this.arcScaleY_;
      var x1 = fillStyle.x1_ / this.arcScaleX_;
      var y1 = fillStyle.y1_ / this.arcScaleY_;
      var p0 = this.getCoords_(x0, y0);
      var p1 = this.getCoords_(x1, y1);
      var dx = p1.x - p0.x;
      var dy = p1.y - p0.y;
      angle = Math.atan2(dx, dy) * 180 / Math.PI;

      // The angle should be a non-negative number.
      if (angle < 0) {
        angle += 360;
      }

      // Very small angles produce an unexpected result because they are
      // converted to a scientific notation string.
      if (angle < 1e-6) {
        angle = 0;
      }
    } else {
      var p0 = this.getCoords_(fillStyle.x0_, fillStyle.y0_);
      var width  = max.x - min.x;
      var height = max.y - min.y;
      focus = {
        x: (p0.x - min.x) / width,
        y: (p0.y - min.y) / height
      };

      width  /= this.arcScaleX_ * Z;
      height /= this.arcScaleY_ * Z;
      var dimension = m.max(width, height);
      shift = 2 * fillStyle.r0_ / dimension;
      expansion = 2 * fillStyle.r1_ / dimension - shift;
    }

    // We need to sort the color stops in ascending order by offset,
    // otherwise IE won't interpret it correctly.
    var stops = fillStyle.colors_;
    stops.sort(function(cs1, cs2) {
      return cs1.offset - cs2.offset;
    });

    var length = stops.length;
    var color1 = stops[0].color;
    var color2 = stops[length - 1].color;
    var opacity1 = stops[0].alpha * this.globalAlpha;
    var opacity2 = stops[length - 1].alpha * this.globalAlpha;

    var colors = [];
    for (var i = 0; i < length; i++) {
      var stop = stops[i];
      colors.push(stop.offset * expansion + shift + ' ' + stop.color);
    }

    // When colors attribute is used, the meanings of opacity and o:opacity2
    // are reversed.
    lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"',
                 ' method="none" focus="100%"',
                 ' color="', color1, '"',
                 ' color2="', color2, '"',
                 ' colors="', colors.join(','), '"',
                 ' opacity="', opacity2, '"',
                 ' g_o_:opacity2="', opacity1, '"',
                 ' angle="', angle, '"',
                 ' focusposition="', focus.x, ',', focus.y, '" />');
  } else {
    lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity,
                 '" />');
  }

  lineStr.push('</g_vml_:shape>');

  this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
};

contextPrototype.fill = function() {
  this.stroke(true);
}

contextPrototype.closePath = function() {
  this.currentPath_.push({type: 'close'});
};

/**
 * @private
 */
contextPrototype.getCoords_ = function(aX, aY) {
  var m = this.m_;
  return {
    x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
    y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
  }
};

contextPrototype.save = function() {
  var o = {};
  copyState(this, o);
  this.aStack_.push(o);
  this.mStack_.push(this.m_);
  this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
};

contextPrototype.restore = function() {
  copyState(this.aStack_.pop(), this);
  this.m_ = this.mStack_.pop();
};

function matrixIsFinite(m) {
  for (var j = 0; j < 3; j++) {
    for (var k = 0; k < 2; k++) {
      if (!isFinite(m[j][k]) || isNaN(m[j][k])) {
        return false;
      }
    }
  }
  return true;
}

function setM(ctx, m, updateLineScale) {
  if (!matrixIsFinite(m)) {
    return;
  }
  ctx.m_ = m;

  if (updateLineScale) {
    // Get the line scale.
    // Determinant of this.m_ means how much the area is enlarged by the
    // transformation. So its square root can be used as a scale factor
    // for width.
    var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
    ctx.lineScale_ = sqrt(abs(det));
  }
}

contextPrototype.translate = function(aX, aY) {
  var m1 = [
    [1,  0,  0],
    [0,  1,  0],
    [aX, aY, 1]
  ];

  setM(this, matrixMultiply(m1, this.m_), false);
};

contextPrototype.rotate = function(aRot) {
  var c = mc(aRot);
  var s = ms(aRot);

  var m1 = [
    [c,  s, 0],
    [-s, c, 0],
    [0,  0, 1]
  ];

  setM(this, matrixMultiply(m1, this.m_), false);
};

contextPrototype.scale = function(aX, aY) {
  this.arcScaleX_ *= aX;
  this.arcScaleY_ *= aY;
  var m1 = [
    [aX, 0,  0],
    [0,  aY, 0],
    [0,  0,  1]
  ];

  setM(this, matrixMultiply(m1, this.m_), true);
};

contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) {
  var m1 = [
    [m11, m12, 0],
    [m21, m22, 0],
    [dx,  dy,  1]
  ];

  setM(this, matrixMultiply(m1, this.m_), true);
};

contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) {
  var m = [
    [m11, m12, 0],
    [m21, m22, 0],
    [dx,  dy,  1]
  ];

  setM(this, m, true);
};

/******** STUBS ********/
contextPrototype.clip = function() {
  // TODO: Implement
};

contextPrototype.arcTo = function() {
  // TODO: Implement
};

contextPrototype.createPattern = function() {
  return new CanvasPattern_;
};

// Gradient / Pattern Stubs
function CanvasGradient_(aType) {
  this.type_ = aType;
  this.x0_ = 0;
  this.y0_ = 0;
  this.r0_ = 0;
  this.x1_ = 0;
  this.y1_ = 0;
  this.r1_ = 0;
  this.colors_ = [];
}

CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
  aColor = processStyle(aColor);
  this.colors_.push({offset: aOffset,
                     color: aColor.color,
                     alpha: aColor.alpha});
};

function CanvasPattern_() {}

// set up externs
G_vmlCanvasManager = G_vmlCanvasManager_;
CanvasRenderingContext2D = CanvasRenderingContext2D_;
CanvasGradient = CanvasGradient_;
CanvasPattern = CanvasPattern_;

})();

} // if