// 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 only support repeat. // * 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. // * Filling very large shapes (above 5000 points) is buggy. // * 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)));
  };
}

function encodeHtmlAttribute(s) {
  return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
}

function addNamespacesAndStylesheet(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}';
  }
}

// Add namespaces and stylesheet at startup.
addNamespacesAndStylesheet(document);

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) {
    // 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;

      // Add namespaces and stylesheet to document of the element.
      addNamespacesAndStylesheet(el.ownerDocument);

      // 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.getContext().clearRect();
      el.style.width = el.attributes.width.nodeValue + 'px';
      // In IE8 this does not trigger onresize.
      el.firstChild.style.width =  el.clientWidth + 'px';
      break;
    case 'height':
      el.getContext().clearRect();
      el.style.height = el.attributes.height.nodeValue + 'px';
      el.firstChild.style.height = el.clientHeight + 'px';
      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 decToHex = [];
for (var i = 0; i < 16; i++) {
  for (var j = 0; j < 16; j++) {
    decToHex[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.font          = o1.font;
  o2.textAlign     = o1.textAlign;
  o2.textBaseline  = o1.textBaseline;
  o2.arcScaleX_    = o1.arcScaleX_;
  o2.arcScaleY_    = o1.arcScaleY_;
  o2.lineScale_    = o1.lineScale_;
}

var colorData = {
  aliceblue: '#F0F8FF',
  antiquewhite: '#FAEBD7',
  aquamarine: '#7FFFD4',
  azure: '#F0FFFF',
  beige: '#F5F5DC',
  bisque: '#FFE4C4',
  black: '#000000',
  blanchedalmond: '#FFEBCD',
  blueviolet: '#8A2BE2',
  brown: '#A52A2A',
  burlywood: '#DEB887',
  cadetblue: '#5F9EA0',
  chartreuse: '#7FFF00',
  chocolate: '#D2691E',
  coral: '#FF7F50',
  cornflowerblue: '#6495ED',
  cornsilk: '#FFF8DC',
  crimson: '#DC143C',
  cyan: '#00FFFF',
  darkblue: '#00008B',
  darkcyan: '#008B8B',
  darkgoldenrod: '#B8860B',
  darkgray: '#A9A9A9',
  darkgreen: '#006400',
  darkgrey: '#A9A9A9',
  darkkhaki: '#BDB76B',
  darkmagenta: '#8B008B',
  darkolivegreen: '#556B2F',
  darkorange: '#FF8C00',
  darkorchid: '#9932CC',
  darkred: '#8B0000',
  darksalmon: '#E9967A',
  darkseagreen: '#8FBC8F',
  darkslateblue: '#483D8B',
  darkslategray: '#2F4F4F',
  darkslategrey: '#2F4F4F',
  darkturquoise: '#00CED1',
  darkviolet: '#9400D3',
  deeppink: '#FF1493',
  deepskyblue: '#00BFFF',
  dimgray: '#696969',
  dimgrey: '#696969',
  dodgerblue: '#1E90FF',
  firebrick: '#B22222',
  floralwhite: '#FFFAF0',
  forestgreen: '#228B22',
  gainsboro: '#DCDCDC',
  ghostwhite: '#F8F8FF',
  gold: '#FFD700',
  goldenrod: '#DAA520',
  grey: '#808080',
  greenyellow: '#ADFF2F',
  honeydew: '#F0FFF0',
  hotpink: '#FF69B4',
  indianred: '#CD5C5C',
  indigo: '#4B0082',
  ivory: '#FFFFF0',
  khaki: '#F0E68C',
  lavender: '#E6E6FA',
  lavenderblush: '#FFF0F5',
  lawngreen: '#7CFC00',
  lemonchiffon: '#FFFACD',
  lightblue: '#ADD8E6',
  lightcoral: '#F08080',
  lightcyan: '#E0FFFF',
  lightgoldenrodyellow: '#FAFAD2',
  lightgreen: '#90EE90',
  lightgrey: '#D3D3D3',
  lightpink: '#FFB6C1',
  lightsalmon: '#FFA07A',
  lightseagreen: '#20B2AA',
  lightskyblue: '#87CEFA',
  lightslategray: '#778899',
  lightslategrey: '#778899',
  lightsteelblue: '#B0C4DE',
  lightyellow: '#FFFFE0',
  limegreen: '#32CD32',
  linen: '#FAF0E6',
  magenta: '#FF00FF',
  mediumaquamarine: '#66CDAA',
  mediumblue: '#0000CD',
  mediumorchid: '#BA55D3',
  mediumpurple: '#9370DB',
  mediumseagreen: '#3CB371',
  mediumslateblue: '#7B68EE',
  mediumspringgreen: '#00FA9A',
  mediumturquoise: '#48D1CC',
  mediumvioletred: '#C71585',
  midnightblue: '#191970',
  mintcream: '#F5FFFA',
  mistyrose: '#FFE4E1',
  moccasin: '#FFE4B5',
  navajowhite: '#FFDEAD',
  oldlace: '#FDF5E6',
  olivedrab: '#6B8E23',
  orange: '#FFA500',
  orangered: '#FF4500',
  orchid: '#DA70D6',
  palegoldenrod: '#EEE8AA',
  palegreen: '#98FB98',
  paleturquoise: '#AFEEEE',
  palevioletred: '#DB7093',
  papayawhip: '#FFEFD5',
  peachpuff: '#FFDAB9',
  peru: '#CD853F',
  pink: '#FFC0CB',
  plum: '#DDA0DD',
  powderblue: '#B0E0E6',
  rosybrown: '#BC8F8F',
  royalblue: '#4169E1',
  saddlebrown: '#8B4513',
  salmon: '#FA8072',
  sandybrown: '#F4A460',
  seagreen: '#2E8B57',
  seashell: '#FFF5EE',
  sienna: '#A0522D',
  skyblue: '#87CEEB',
  slateblue: '#6A5ACD',
  slategray: '#708090',
  slategrey: '#708090',
  snow: '#FFFAFA',
  springgreen: '#00FF7F',
  steelblue: '#4682B4',
  tan: '#D2B48C',
  thistle: '#D8BFD8',
  tomato: '#FF6347',
  turquoise: '#40E0D0',
  violet: '#EE82EE',
  wheat: '#F5DEB3',
  whitesmoke: '#F5F5F5',
  yellowgreen: '#9ACD32'
};

function getRgbHslContent(styleString) {
  var start = styleString.indexOf('(', 3);
  var end = styleString.indexOf(')', start + 1);
  var parts = styleString.substring(start + 1, end).split(',');
  // add alpha if needed
  if (parts.length == 4 && styleString.substr(3, 1) == 'a') {
    alpha = Number(parts[3]);
  } else {
    parts[3] = 1;
  }
  return parts;
}

function percent(s) {
  return parseFloat(s) / 100;
}

function clamp(v, min, max) {
  return Math.min(max, Math.max(min, v));
}

function hslToRgb(parts){
  var r, g, b;
  h = parseFloat(parts[0]) / 360 % 360;
  if (h < 0)
    h++;
  s = clamp(percent(parts[1]), 0, 1);
  l = clamp(percent(parts[2]), 0, 1);
  if (s == 0) {
    r = g = b = l; // achromatic
  } else {
    var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    var p = 2 * l - q;
    r = hueToRgb(p, q, h + 1 / 3);
    g = hueToRgb(p, q, h);
    b = hueToRgb(p, q, h - 1 / 3);
  }

  return '#' + decToHex[Math.floor(r * 255)] +
      decToHex[Math.floor(g * 255)] +
      decToHex[Math.floor(b * 255)];
}

function hueToRgb(m1, m2, h) {
  if (h < 0)
    h++;
  if (h > 1)
    h--;

  if (6 * h < 1)
    return m1 + (m2 - m1) * 6 * h;
  else if (2 * h < 1)
    return m2;
  else if (3 * h < 2)
    return m1 + (m2 - m1) * (2 / 3 - h) * 6;
  else
    return m1;
}

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

  styleString = String(styleString);
  if (styleString.charAt(0) == '#') {
    str = styleString;
  } else if (/^rgb/.test(styleString)) {
    var parts = getRgbHslContent(styleString);
    var str = '#', n;
    for (var i = 0; i < 3; i++) {
      if (parts[i].indexOf('%') != -1) {
        n = Math.floor(percent(parts[i]) * 255);
      } else {
        n = Number(parts[i]);
      }
      str += decToHex[clamp(n, 0, 255)];
    }
    alpha = parts[3];
  } else if (/^hsl/.test(styleString)) {
    var parts = getRgbHslContent(styleString);
    str = hslToRgb(parts);
    alpha = parts[3];
  } else {
    str = colorData[styleString] || styleString;
  }
  return {color: str, alpha: alpha};
}

var DEFAULT_STYLE = {
  style: 'normal',
  variant: 'normal',
  weight: 'normal',
  size: 10,
  family: 'sans-serif'
};

// Internal text style cache
var fontStyleCache = {};

function processFontStyle(styleString) {
  if (fontStyleCache[styleString]) {
    return fontStyleCache[styleString];
  }

  var el = document.createElement('div');
  var style = el.style;
  try {
    style.font = styleString;
  } catch (ex) {
    // Ignore failures to set to invalid font.
  }

  return fontStyleCache[styleString] = {
    style: style.fontStyle || DEFAULT_STYLE.style,
    variant: style.fontVariant || DEFAULT_STYLE.variant,
    weight: style.fontWeight || DEFAULT_STYLE.weight,
    size: style.fontSize || DEFAULT_STYLE.size,
    family: style.fontFamily || DEFAULT_STYLE.family
  };
}

function getComputedStyle(style, element) {
  var computedStyle = {};

  for (var p in style) {
    computedStyle[p] = style[p];
  }

  // Compute the size
  var canvasFontSize = parseFloat(element.currentStyle.fontSize),
      fontSize = parseFloat(style.size);

  if (typeof style.size == 'number') {
    computedStyle.size = style.size;
  } else if (style.size.indexOf('px') != -1) {
    computedStyle.size = fontSize;
  } else if (style.size.indexOf('em') != -1) {
    computedStyle.size = canvasFontSize * fontSize;
  } else if(style.size.indexOf('%') != -1) {
    computedStyle.size = (canvasFontSize / 100) * fontSize;
  } else if (style.size.indexOf('pt') != -1) {
    computedStyle.size = fontSize / .75;
  } else {
    computedStyle.size = canvasFontSize;
  }

  // Different scaling between normal text and VML text. This was found using
  // trial and error to get the same size as non VML text.
  computedStyle.size *= 0.981;

  return computedStyle;
}

function buildStyle(style) {
  return style.style + ' ' + style.variant + ' ' + style.weight + ' ' +
      style.size + 'px ' + style.family;
}

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.font = '10px sans-serif';
  this.textAlign = 'left';
  this.textBaseline = 'alphabetic';
  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() {
  if (this.textMeasureEl_) {
    this.textMeasureEl_.removeNode(true);
    this.textMeasureEl_ = null;
  }
  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] ||
      this.m_[1][1] != 1 || this.m_[1][0]) {
    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 W = 10;
  var H = 10;
  // Divide the shape into chunks if it's too long because IE has a limit
  // somewhere for how long a VML shape can be. This simple division does
  // not work with fills, only strokes, unfortunately.
  var chunkSize = 5000;

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

  for (var j = 0; j < this.currentPath_.length; j += chunkSize) {
    var lineStr = [];
    var lineOpen = false;

    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;

    for (var i = j; i < Math.min(j + chunkSize, this.currentPath_.length); i++) {
      if (i % chunkSize == 0 && i > 0) { // move into position for next chunk
        lineStr.push(' m ', mr(this.currentPath_[i-1].x), ',', mr(this.currentPath_[i-1].y));
      }

      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) {
      appendStroke(this, lineStr);
    } else {
      appendFill(this, lineStr, min, max);
    }

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

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

function appendStroke(ctx, lineStr) {
  var a = processStyle(ctx.strokeStyle);
  var color = a.color;
  var opacity = a.alpha * ctx.globalAlpha;
  var lineWidth = ctx.lineScale_ * ctx.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="', ctx.lineJoin, '"',
    ' miterlimit="', ctx.miterLimit, '"',
    ' endcap="', processLineCap(ctx.lineCap), '"',
    ' weight="', lineWidth, 'px"',
    ' color="', color, '" />'
  );
}

function appendFill(ctx, lineStr, min, max) {
  var fillStyle = ctx.fillStyle;
  var arcScaleX = ctx.arcScaleX_;
  var arcScaleY = ctx.arcScaleY_;
  var width = max.x - min.x;
  var height = max.y - min.y;
  if (fillStyle instanceof CanvasGradient_) {
    // TODO: Gradients transformed with the transformation matrix.
    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_ / arcScaleX;
      var y0 = fillStyle.y0_ / arcScaleY;
      var x1 = fillStyle.x1_ / arcScaleX;
      var y1 = fillStyle.y1_ / arcScaleY;
      var p0 = ctx.getCoords_(x0, y0);
      var p1 = ctx.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 = ctx.getCoords_(fillStyle.x0_, fillStyle.y0_);
      focus = {
        x: (p0.x - min.x) / width,
        y: (p0.y - min.y) / height
      };

      width  /= arcScaleX * Z;
      height /= 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 * ctx.globalAlpha;
    var opacity2 = stops[length - 1].alpha * ctx.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 if (fillStyle instanceof CanvasPattern_) {
    if (width && height) {
      var deltaLeft = -min.x;
      var deltaTop = -min.y;
      lineStr.push('<g_vml_:fill',
                   ' position="',
                   deltaLeft / width * arcScaleX * arcScaleX, ',',
                   deltaTop / height * arcScaleY * arcScaleY, '"',
                   ' type="tile"',
                   // TODO: Figure out the correct size to fit the scale.
                   //' size="', w, 'px ', h, 'px"',
                   ' src="', fillStyle.src_, '" />');
     }
  } else {
    var a = processStyle(ctx.fillStyle);
    var color = a.color;
    var opacity = a.alpha * ctx.globalAlpha;
    lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity,
                 '" />');
  }
}

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() {
  if (this.aStack_.length) {
    copyState(this.aStack_.pop(), this);
    this.m_ = this.mStack_.pop();
  }
};

function matrixIsFinite(m) {
  return isFinite(m[0][0]) && isFinite(m[0][1]) &&
      isFinite(m[1][0]) && isFinite(m[1][1]) &&
      isFinite(m[2][0]) && isFinite(m[2][1]);
}

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);
};

/**
 * The text drawing function.
 * The maxWidth argument isn't taken in account, since no browser supports
 * it yet.
 */
contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) {
  var m = this.m_,
      delta = 1000,
      left = 0,
      right = delta,
      offset = {x: 0, y: 0},
      lineStr = [];

  var fontStyle = getComputedStyle(processFontStyle(this.font),
                                   this.element_);

  var fontStyleString = buildStyle(fontStyle);

  var elementStyle = this.element_.currentStyle;
  var textAlign = this.textAlign.toLowerCase();
  switch (textAlign) {
    case 'left':
    case 'center':
    case 'right':
      break;
    case 'end':
      textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left';
      break;
    case 'start':
      textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left';
      break;
    default:
      textAlign = 'left';
  }

  // 1.75 is an arbitrary number, as there is no info about the text baseline
  switch (this.textBaseline) {
    case 'hanging':
    case 'top':
      offset.y = fontStyle.size / 1.75;
      break;
    case 'middle':
      break;
    default:
    case null:
    case 'alphabetic':
    case 'ideographic':
    case 'bottom':
      offset.y = -fontStyle.size / 2.25;
      break;
  }

  switch(textAlign) {
    case 'right':
      left = delta;
      right = 0.05;
      break;
    case 'center':
      left = right = delta / 2;
      break;
  }

  var d = this.getCoords_(x + offset.x, y + offset.y);

  lineStr.push('<g_vml_:line from="', -left ,' 0" to="', right ,' 0.05" ',
               ' coordsize="100 100" coordorigin="0 0"',
               ' filled="', !stroke, '" stroked="', !!stroke,
               '" style="position:absolute;width:1px;height:1px;">');

  if (stroke) {
    appendStroke(this, lineStr);
  } else {
    // TODO: Fix the min and max params.
    appendFill(this, lineStr, {x: -left, y: 0},
               {x: right, y: fontStyle.size});
  }

  var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' +
              m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0';

  var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z);

  lineStr.push('<g_vml_:skew on="t" matrix="', skewM ,'" ',
               ' offset="', skewOffset, '" origin="', left ,' 0" />',
               '<g_vml_:path textpathok="true" />',
               '<g_vml_:textpath on="true" string="',
               encodeHtmlAttribute(text),
               '" style="v-text-align:', textAlign,
               ';font:', encodeHtmlAttribute(fontStyleString),
               '" /></g_vml_:line>');

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

contextPrototype.fillText = function(text, x, y, maxWidth) {
  this.drawText_(text, x, y, maxWidth, false);
};

contextPrototype.strokeText = function(text, x, y, maxWidth) {
  this.drawText_(text, x, y, maxWidth, true);
};

contextPrototype.measureText = function(text) {
  if (!this.textMeasureEl_) {
    var s = '<span style="position:absolute;' +
        'top:-20000px;left:0;padding:0;margin:0;border:none;' +
        'white-space:pre;"></span>';
    this.element_.insertAdjacentHTML('beforeEnd', s);
    this.textMeasureEl_ = this.element_.lastChild;
  }
  var doc = this.element_.ownerDocument;
  this.textMeasureEl_.innerHTML = '';
  this.textMeasureEl_.style.font = this.font;
  // Don't use innerHTML or innerText because they allow markup/whitespace.
  this.textMeasureEl_.appendChild(doc.createTextNode(text));
  return {width: this.textMeasureEl_.offsetWidth};
};

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

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

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

// 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_(image, repetition) {
  assertImageIsValid(image);
  switch (repetition) {
    case 'repeat':
    case null:
    case '':
      this.repetition_ = 'repeat';
      break
    case 'repeat-x':
    case 'repeat-y':
    case 'no-repeat':
      this.repetition_ = repetition;
      break;
    default:
      throwException('SYNTAX_ERR');
  }

  this.src_ = image.src;
  this.width_ = image.width;
  this.height_ = image.height;
}

function throwException(s) {
  throw new DOMException_(s);
}

function assertImageIsValid(img) {
  if (!img || img.nodeType != 1 || img.tagName != 'IMG') {
    throwException('TYPE_MISMATCH_ERR');
  }
  if (img.readyState != 'complete') {
    throwException('INVALID_STATE_ERR');
  }
}

function DOMException_(s) {
  this.code = this[s];
  this.message = s +': DOM Exception ' + this.code;
}
var p = DOMException_.prototype = new Error;
p.INDEX_SIZE_ERR = 1;
p.DOMSTRING_SIZE_ERR = 2;
p.HIERARCHY_REQUEST_ERR = 3;
p.WRONG_DOCUMENT_ERR = 4;
p.INVALID_CHARACTER_ERR = 5;
p.NO_DATA_ALLOWED_ERR = 6;
p.NO_MODIFICATION_ALLOWED_ERR = 7;
p.NOT_FOUND_ERR = 8;
p.NOT_SUPPORTED_ERR = 9;
p.INUSE_ATTRIBUTE_ERR = 10;
p.INVALID_STATE_ERR = 11;
p.SYNTAX_ERR = 12;
p.INVALID_MODIFICATION_ERR = 13;
p.NAMESPACE_ERR = 14;
p.INVALID_ACCESS_ERR = 15;
p.VALIDATION_ERR = 16;
p.TYPE_MISMATCH_ERR = 17;

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

})();

} // if