mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
1194 lines
41 KiB
JavaScript
1194 lines
41 KiB
JavaScript
/*
|
|
* FlashCanvas
|
|
*
|
|
* Copyright (c) 2009 Tim Cameron Ryan
|
|
* Copyright (c) 2009-2011 FlashCanvas Project
|
|
* Released under the MIT/X License
|
|
*/
|
|
|
|
// Reference:
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html
|
|
// http://dev.w3.org/html5/spec/the-canvas-element.html
|
|
|
|
// If the browser is IE and does not support HTML5 Canvas
|
|
if (window["ActiveXObject"] && !window["CanvasRenderingContext2D"]) {
|
|
|
|
(function(window, document, undefined) {
|
|
|
|
/*
|
|
* Constant
|
|
*/
|
|
|
|
var NULL = null;
|
|
var CANVAS = "canvas";
|
|
var CANVAS_RENDERING_CONTEXT_2D = "CanvasRenderingContext2D";
|
|
var CANVAS_GRADIENT = "CanvasGradient";
|
|
var CANVAS_PATTERN = "CanvasPattern";
|
|
var FLASH_CANVAS = "FlashCanvas";
|
|
var G_VML_CANVAS_MANAGER = "G_vmlCanvasManager";
|
|
var OBJECT_ID_PREFIX = "external";
|
|
var ON_FOCUS = "onfocus";
|
|
var ON_PROPERTY_CHANGE = "onpropertychange";
|
|
var ON_READY_STATE_CHANGE = "onreadystatechange";
|
|
var ON_UNLOAD = "onunload";
|
|
|
|
var config = window[FLASH_CANVAS + "Options"] || {};
|
|
var BASE_URL = config["swfPath"] || getScriptUrl().replace(/[^\/]+$/, "");
|
|
var SWF_URL = BASE_URL + "flashcanvas.swf";
|
|
|
|
// DOMException code
|
|
var INDEX_SIZE_ERR = 1;
|
|
var NOT_SUPPORTED_ERR = 9;
|
|
var INVALID_STATE_ERR = 11;
|
|
var SYNTAX_ERR = 12;
|
|
var TYPE_MISMATCH_ERR = 17;
|
|
var SECURITY_ERR = 18;
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function Lookup(array) {
|
|
for (var i = 0, n = array.length; i < n; i++)
|
|
this[array[i]] = i;
|
|
}
|
|
|
|
var properties = new Lookup([
|
|
// Canvas element
|
|
"toDataURL",
|
|
|
|
// CanvasRenderingContext2D
|
|
"save",
|
|
"restore",
|
|
"scale",
|
|
"rotate",
|
|
"translate",
|
|
"transform",
|
|
"setTransform",
|
|
"globalAlpha",
|
|
"globalCompositeOperation",
|
|
"strokeStyle",
|
|
"fillStyle",
|
|
"createLinearGradient",
|
|
"createRadialGradient",
|
|
"createPattern",
|
|
"lineWidth",
|
|
"lineCap",
|
|
"lineJoin",
|
|
"miterLimit",
|
|
"shadowOffsetX",
|
|
"shadowOffsetY",
|
|
"shadowBlur",
|
|
"shadowColor",
|
|
"clearRect",
|
|
"fillRect",
|
|
"strokeRect",
|
|
"beginPath",
|
|
"closePath",
|
|
"moveTo",
|
|
"lineTo",
|
|
"quadraticCurveTo",
|
|
"bezierCurveTo",
|
|
"arcTo",
|
|
"rect",
|
|
"arc",
|
|
"fill",
|
|
"stroke",
|
|
"clip",
|
|
"isPointInPath",
|
|
// "drawFocusRing",
|
|
"font",
|
|
"textAlign",
|
|
"textBaseline",
|
|
"fillText",
|
|
"strokeText",
|
|
"measureText",
|
|
"drawImage",
|
|
"createImageData",
|
|
"getImageData",
|
|
"putImageData",
|
|
|
|
// CanvasGradient
|
|
"addColorStop",
|
|
|
|
// Internal use
|
|
"direction",
|
|
"resize"
|
|
]);
|
|
|
|
// Whether swf is ready for use
|
|
var isReady = {};
|
|
|
|
// Cache of images loaded by createPattern() or drawImage()
|
|
var images = {};
|
|
|
|
// Monitor the number of loading files
|
|
var lock = {};
|
|
|
|
// Callback functions passed to loadImage()
|
|
var callbacks = {};
|
|
|
|
// Canvas elements
|
|
var canvases = {};
|
|
|
|
// SPAN element embedded in the canvas
|
|
var spans = {};
|
|
|
|
/**
|
|
* 2D context
|
|
* @constructor
|
|
*/
|
|
var CanvasRenderingContext2D = function(canvas, swf) {
|
|
// back-reference to the canvas
|
|
this.canvas = canvas;
|
|
|
|
// back-reference to the swf
|
|
this._swf = swf;
|
|
|
|
// unique ID of canvas
|
|
this._canvasId = swf.id.slice(8);
|
|
|
|
// initialize drawing states
|
|
this._initialize();
|
|
|
|
// Count CanvasGradient and CanvasPattern objects
|
|
this._gradientPatternId = 0;
|
|
|
|
// Directionality of the canvas element
|
|
this._direction = "";
|
|
|
|
// This ensures that font properties of the canvas element is
|
|
// transmitted to Flash.
|
|
this._font = "";
|
|
|
|
// frame update interval
|
|
var self = this;
|
|
setInterval(function() {
|
|
if (lock[self._canvasId] === 0) {
|
|
self._executeCommand();
|
|
}
|
|
}, 30);
|
|
};
|
|
|
|
CanvasRenderingContext2D.prototype = {
|
|
/*
|
|
* state
|
|
*/
|
|
|
|
save: function() {
|
|
// write all properties
|
|
this._setCompositing();
|
|
this._setShadows();
|
|
this._setStrokeStyle();
|
|
this._setFillStyle();
|
|
this._setLineStyles();
|
|
this._setFontStyles();
|
|
|
|
// push state
|
|
this._stateStack.push([
|
|
this._globalAlpha,
|
|
this._globalCompositeOperation,
|
|
this._strokeStyle,
|
|
this._fillStyle,
|
|
this._lineWidth,
|
|
this._lineCap,
|
|
this._lineJoin,
|
|
this._miterLimit,
|
|
this._shadowOffsetX,
|
|
this._shadowOffsetY,
|
|
this._shadowBlur,
|
|
this._shadowColor,
|
|
this._font,
|
|
this._textAlign,
|
|
this._textBaseline
|
|
]);
|
|
|
|
this._queue.push(properties.save);
|
|
},
|
|
|
|
restore: function() {
|
|
// pop state
|
|
var stateStack = this._stateStack;
|
|
if (stateStack.length) {
|
|
var state = stateStack.pop();
|
|
this.globalAlpha = state[0];
|
|
this.globalCompositeOperation = state[1];
|
|
this.strokeStyle = state[2];
|
|
this.fillStyle = state[3];
|
|
this.lineWidth = state[4];
|
|
this.lineCap = state[5];
|
|
this.lineJoin = state[6];
|
|
this.miterLimit = state[7];
|
|
this.shadowOffsetX = state[8];
|
|
this.shadowOffsetY = state[9];
|
|
this.shadowBlur = state[10];
|
|
this.shadowColor = state[11];
|
|
this.font = state[12];
|
|
this.textAlign = state[13];
|
|
this.textBaseline = state[14];
|
|
}
|
|
|
|
this._queue.push(properties.restore);
|
|
},
|
|
|
|
/*
|
|
* transformations
|
|
*/
|
|
|
|
scale: function(x, y) {
|
|
this._queue.push(properties.scale, x, y);
|
|
},
|
|
|
|
rotate: function(angle) {
|
|
this._queue.push(properties.rotate, angle);
|
|
},
|
|
|
|
translate: function(x, y) {
|
|
this._queue.push(properties.translate, x, y);
|
|
},
|
|
|
|
transform: function(m11, m12, m21, m22, dx, dy) {
|
|
this._queue.push(properties.transform, m11, m12, m21, m22, dx, dy);
|
|
},
|
|
|
|
setTransform: function(m11, m12, m21, m22, dx, dy) {
|
|
this._queue.push(properties.setTransform, m11, m12, m21, m22, dx, dy);
|
|
},
|
|
|
|
/*
|
|
* compositing
|
|
*/
|
|
|
|
_setCompositing: function() {
|
|
var queue = this._queue;
|
|
if (this._globalAlpha !== this.globalAlpha) {
|
|
this._globalAlpha = this.globalAlpha;
|
|
queue.push(properties.globalAlpha, this._globalAlpha);
|
|
}
|
|
if (this._globalCompositeOperation !== this.globalCompositeOperation) {
|
|
this._globalCompositeOperation = this.globalCompositeOperation;
|
|
queue.push(properties.globalCompositeOperation, this._globalCompositeOperation);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* colors and styles
|
|
*/
|
|
|
|
_setStrokeStyle: function() {
|
|
if (this._strokeStyle !== this.strokeStyle) {
|
|
var style = this._strokeStyle = this.strokeStyle;
|
|
if (typeof style === "string") {
|
|
// OK
|
|
} else if (style instanceof CanvasGradient ||
|
|
style instanceof CanvasPattern) {
|
|
style = style.id;
|
|
} else {
|
|
return;
|
|
}
|
|
this._queue.push(properties.strokeStyle, style);
|
|
}
|
|
},
|
|
|
|
_setFillStyle: function() {
|
|
if (this._fillStyle !== this.fillStyle) {
|
|
var style = this._fillStyle = this.fillStyle;
|
|
if (typeof style === "string") {
|
|
// OK
|
|
} else if (style instanceof CanvasGradient ||
|
|
style instanceof CanvasPattern) {
|
|
style = style.id;
|
|
} else {
|
|
return;
|
|
}
|
|
this._queue.push(properties.fillStyle, style);
|
|
}
|
|
},
|
|
|
|
createLinearGradient: function(x0, y0, x1, y1) {
|
|
// If any of the arguments are not finite numbers, throws a
|
|
// NOT_SUPPORTED_ERR exception.
|
|
if (!(isFinite(x0) && isFinite(y0) && isFinite(x1) && isFinite(y1))) {
|
|
throwException(NOT_SUPPORTED_ERR);
|
|
}
|
|
|
|
this._queue.push(properties.createLinearGradient, x0, y0, x1, y1);
|
|
return new CanvasGradient(this);
|
|
},
|
|
|
|
createRadialGradient: function(x0, y0, r0, x1, y1, r1) {
|
|
// If any of the arguments are not finite numbers, throws a
|
|
// NOT_SUPPORTED_ERR exception.
|
|
if (!(isFinite(x0) && isFinite(y0) && isFinite(r0) &&
|
|
isFinite(x1) && isFinite(y1) && isFinite(r1))) {
|
|
throwException(NOT_SUPPORTED_ERR);
|
|
}
|
|
|
|
// If either of the radii are negative, throws an INDEX_SIZE_ERR
|
|
// exception.
|
|
if (r0 < 0 || r1 < 0) {
|
|
throwException(INDEX_SIZE_ERR);
|
|
}
|
|
|
|
this._queue.push(properties.createRadialGradient, x0, y0, r0, x1, y1, r1);
|
|
return new CanvasGradient(this);
|
|
},
|
|
|
|
createPattern: function(image, repetition) {
|
|
// If the image is null, the implementation must raise a
|
|
// TYPE_MISMATCH_ERR exception.
|
|
if (!image) {
|
|
throwException(TYPE_MISMATCH_ERR);
|
|
}
|
|
|
|
var tagName = image.tagName, src;
|
|
var canvasId = this._canvasId;
|
|
|
|
// If the first argument isn't an img, canvas, or video element,
|
|
// throws a TYPE_MISMATCH_ERR exception.
|
|
if (tagName) {
|
|
tagName = tagName.toLowerCase();
|
|
if (tagName === "img") {
|
|
src = image.getAttribute("src", 2);
|
|
} else if (tagName === CANVAS || tagName === "video") {
|
|
// For now, only HTMLImageElement is supported.
|
|
return;
|
|
} else {
|
|
throwException(TYPE_MISMATCH_ERR);
|
|
}
|
|
}
|
|
|
|
// Additionally, we accept any object that has a src property.
|
|
// This is useful when you'd like to specify a long data URI.
|
|
else if (image.src) {
|
|
src = image.src;
|
|
} else {
|
|
throwException(TYPE_MISMATCH_ERR);
|
|
}
|
|
|
|
// If the second argument isn't one of the allowed values, throws a
|
|
// SYNTAX_ERR exception.
|
|
if (!(repetition === "repeat" || repetition === "no-repeat" ||
|
|
repetition === "repeat-x" || repetition === "repeat-y" ||
|
|
repetition === "" || repetition === NULL)) {
|
|
throwException(SYNTAX_ERR);
|
|
}
|
|
|
|
// Special characters in the filename need escaping.
|
|
this._queue.push(properties.createPattern, encodeXML(src), repetition);
|
|
|
|
// If this is the first time to access the URL, the canvas should be
|
|
// locked while the image is being loaded asynchronously.
|
|
if (!images[canvasId][src] && isReady[canvasId]) {
|
|
this._executeCommand();
|
|
++lock[canvasId];
|
|
images[canvasId][src] = true;
|
|
}
|
|
|
|
return new CanvasPattern(this);
|
|
},
|
|
|
|
/*
|
|
* line caps/joins
|
|
*/
|
|
|
|
_setLineStyles: function() {
|
|
var queue = this._queue;
|
|
if (this._lineWidth !== this.lineWidth) {
|
|
this._lineWidth = this.lineWidth;
|
|
queue.push(properties.lineWidth, this._lineWidth);
|
|
}
|
|
if (this._lineCap !== this.lineCap) {
|
|
this._lineCap = this.lineCap;
|
|
queue.push(properties.lineCap, this._lineCap);
|
|
}
|
|
if (this._lineJoin !== this.lineJoin) {
|
|
this._lineJoin = this.lineJoin;
|
|
queue.push(properties.lineJoin, this._lineJoin);
|
|
}
|
|
if (this._miterLimit !== this.miterLimit) {
|
|
this._miterLimit = this.miterLimit;
|
|
queue.push(properties.miterLimit, this._miterLimit);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* shadows
|
|
*/
|
|
|
|
_setShadows: function() {
|
|
var queue = this._queue;
|
|
if (this._shadowOffsetX !== this.shadowOffsetX) {
|
|
this._shadowOffsetX = this.shadowOffsetX;
|
|
queue.push(properties.shadowOffsetX, this._shadowOffsetX);
|
|
}
|
|
if (this._shadowOffsetY !== this.shadowOffsetY) {
|
|
this._shadowOffsetY = this.shadowOffsetY;
|
|
queue.push(properties.shadowOffsetY, this._shadowOffsetY);
|
|
}
|
|
if (this._shadowBlur !== this.shadowBlur) {
|
|
this._shadowBlur = this.shadowBlur;
|
|
queue.push(properties.shadowBlur, this._shadowBlur);
|
|
}
|
|
if (this._shadowColor !== this.shadowColor) {
|
|
this._shadowColor = this.shadowColor;
|
|
queue.push(properties.shadowColor, this._shadowColor);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* rects
|
|
*/
|
|
|
|
clearRect: function(x, y, w, h) {
|
|
this._queue.push(properties.clearRect, x, y, w, h);
|
|
},
|
|
|
|
fillRect: function(x, y, w, h) {
|
|
this._setCompositing();
|
|
this._setShadows();
|
|
this._setFillStyle();
|
|
this._queue.push(properties.fillRect, x, y, w, h);
|
|
},
|
|
|
|
strokeRect: function(x, y, w, h) {
|
|
this._setCompositing();
|
|
this._setShadows();
|
|
this._setStrokeStyle();
|
|
this._setLineStyles();
|
|
this._queue.push(properties.strokeRect, x, y, w, h);
|
|
},
|
|
|
|
/*
|
|
* path API
|
|
*/
|
|
|
|
beginPath: function() {
|
|
this._queue.push(properties.beginPath);
|
|
},
|
|
|
|
closePath: function() {
|
|
this._queue.push(properties.closePath);
|
|
},
|
|
|
|
moveTo: function(x, y) {
|
|
this._queue.push(properties.moveTo, x, y);
|
|
},
|
|
|
|
lineTo: function(x, y) {
|
|
this._queue.push(properties.lineTo, x, y);
|
|
},
|
|
|
|
quadraticCurveTo: function(cpx, cpy, x, y) {
|
|
this._queue.push(properties.quadraticCurveTo, cpx, cpy, x, y);
|
|
},
|
|
|
|
bezierCurveTo: function(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
this._queue.push(properties.bezierCurveTo, cp1x, cp1y, cp2x, cp2y, x, y);
|
|
},
|
|
|
|
arcTo: function(x1, y1, x2, y2, radius) {
|
|
// Throws an INDEX_SIZE_ERR exception if the given radius is negative.
|
|
if (radius < 0 && isFinite(radius)) {
|
|
throwException(INDEX_SIZE_ERR);
|
|
}
|
|
|
|
this._queue.push(properties.arcTo, x1, y1, x2, y2, radius);
|
|
},
|
|
|
|
rect: function(x, y, w, h) {
|
|
this._queue.push(properties.rect, x, y, w, h);
|
|
},
|
|
|
|
arc: function(x, y, radius, startAngle, endAngle, anticlockwise) {
|
|
// Throws an INDEX_SIZE_ERR exception if the given radius is negative.
|
|
if (radius < 0 && isFinite(radius)) {
|
|
throwException(INDEX_SIZE_ERR);
|
|
}
|
|
|
|
this._queue.push(properties.arc, x, y, radius, startAngle, endAngle, anticlockwise ? 1 : 0);
|
|
},
|
|
|
|
fill: function() {
|
|
this._setCompositing();
|
|
this._setShadows();
|
|
this._setFillStyle();
|
|
this._queue.push(properties.fill);
|
|
},
|
|
|
|
stroke: function() {
|
|
this._setCompositing();
|
|
this._setShadows();
|
|
this._setStrokeStyle();
|
|
this._setLineStyles();
|
|
this._queue.push(properties.stroke);
|
|
},
|
|
|
|
clip: function() {
|
|
this._queue.push(properties.clip);
|
|
},
|
|
|
|
isPointInPath: function(x, y) {
|
|
// TODO: Implement
|
|
},
|
|
|
|
/*
|
|
* text
|
|
*/
|
|
|
|
_setFontStyles: function() {
|
|
var queue = this._queue;
|
|
if (this._font !== this.font) {
|
|
try {
|
|
var span = spans[this._canvasId];
|
|
span.style.font = this._font = this.font;
|
|
|
|
var style = span.currentStyle;
|
|
var fontSize = span.offsetHeight;
|
|
var font = [style.fontStyle, style.fontWeight, fontSize, style.fontFamily].join(" ");
|
|
queue.push(properties.font, font);
|
|
} catch(e) {
|
|
// If this.font cannot be parsed as a CSS font value, then it
|
|
// must be ignored.
|
|
}
|
|
}
|
|
if (this._textAlign !== this.textAlign) {
|
|
this._textAlign = this.textAlign;
|
|
queue.push(properties.textAlign, this._textAlign);
|
|
}
|
|
if (this._textBaseline !== this.textBaseline) {
|
|
this._textBaseline = this.textBaseline;
|
|
queue.push(properties.textBaseline, this._textBaseline);
|
|
}
|
|
if (this._direction !== this.canvas.currentStyle.direction) {
|
|
this._direction = this.canvas.currentStyle.direction;
|
|
queue.push(properties.direction, this._direction);
|
|
}
|
|
},
|
|
|
|
fillText: function(text, x, y, maxWidth) {
|
|
this._setCompositing();
|
|
this._setFillStyle();
|
|
this._setShadows();
|
|
this._setFontStyles();
|
|
this._queue.push(properties.fillText, encodeXML(text), x, y,
|
|
maxWidth === undefined ? Infinity : maxWidth);
|
|
},
|
|
|
|
strokeText: function(text, x, y, maxWidth) {
|
|
this._setCompositing();
|
|
this._setStrokeStyle();
|
|
this._setShadows();
|
|
this._setFontStyles();
|
|
this._queue.push(properties.strokeText, encodeXML(text), x, y,
|
|
maxWidth === undefined ? Infinity : maxWidth);
|
|
},
|
|
|
|
measureText: function(text) {
|
|
var span = spans[this._canvasId];
|
|
try {
|
|
span.style.font = this.font;
|
|
} catch(e) {
|
|
// If this.font cannot be parsed as a CSS font value, then it must
|
|
// be ignored.
|
|
}
|
|
|
|
// Replace space characters with tab characters because innerText
|
|
// removes trailing white spaces.
|
|
span.innerText = text.replace(/[ \n\f\r]/g, "\t");
|
|
|
|
return new TextMetrics(span.offsetWidth);
|
|
},
|
|
|
|
/*
|
|
* drawing images
|
|
*/
|
|
|
|
drawImage: function(image, x1, y1, w1, h1, x2, y2, w2, h2) {
|
|
// If the image is null, the implementation must raise a
|
|
// TYPE_MISMATCH_ERR exception.
|
|
|
|
if (!image) {
|
|
throwException(TYPE_MISMATCH_ERR);
|
|
}
|
|
|
|
var tagName = image.tagName, src, argc = arguments.length;
|
|
var canvasId = this._canvasId;
|
|
|
|
// If the first argument isn't an img, canvas, or video element,
|
|
// throws a TYPE_MISMATCH_ERR exception.
|
|
if (tagName) {
|
|
tagName = tagName.toLowerCase();
|
|
if (tagName === "img") {
|
|
src = image.getAttribute("src", 2);
|
|
} else if (tagName === CANVAS || tagName === "video") {
|
|
// For now, only HTMLImageElement is supported.
|
|
return;
|
|
} else {
|
|
throwException(TYPE_MISMATCH_ERR);
|
|
}
|
|
}
|
|
|
|
// Additionally, we accept any object that has a src property.
|
|
// This is useful when you'd like to specify a long data URI.
|
|
else if (image.src) {
|
|
src = image.src;
|
|
} else {
|
|
throwException(TYPE_MISMATCH_ERR);
|
|
}
|
|
|
|
this._setCompositing();
|
|
this._setShadows();
|
|
|
|
// Special characters in the filename need escaping.
|
|
src = encodeXML(src);
|
|
|
|
if (argc === 3) {
|
|
this._queue.push(properties.drawImage, argc, src, x1, y1);
|
|
} else if (argc === 5) {
|
|
this._queue.push(properties.drawImage, argc, src, x1, y1, w1, h1);
|
|
} else if (argc === 9) {
|
|
// If one of the sw or sh arguments is zero, the implementation
|
|
// must raise an INDEX_SIZE_ERR exception.
|
|
if (w1 === 0 || h1 === 0) {
|
|
throwException(INDEX_SIZE_ERR);
|
|
}
|
|
|
|
this._queue.push(properties.drawImage, argc, src, x1, y1, w1, h1, x2, y2, w2, h2);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
// If this is the first time to access the URL, the canvas should be
|
|
// locked while the image is being loaded asynchronously.
|
|
if (!images[canvasId][src] && isReady[canvasId]) {
|
|
this._executeCommand();
|
|
++lock[canvasId];
|
|
images[canvasId][src] = true;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* pixel manipulation
|
|
*/
|
|
|
|
// ImageData createImageData(in float sw, in float sh);
|
|
// ImageData createImageData(in ImageData imagedata);
|
|
createImageData: function() {
|
|
// TODO: Implement
|
|
},
|
|
|
|
// ImageData getImageData(in float sx, in float sy, in float sw, in float sh);
|
|
getImageData: function(sx, sy, sw, sh) {
|
|
// TODO: Implement
|
|
},
|
|
|
|
// void putImageData(in ImageData imagedata, in float dx, in float dy, [Optional] in float dirtyX, in float dirtyY, in float dirtyWidth, in float dirtyHeight);
|
|
putImageData: function(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
|
|
// TODO: Implement
|
|
},
|
|
|
|
/*
|
|
* extended functions
|
|
*/
|
|
|
|
loadImage: function(image, onload, onerror) {
|
|
var tagName = image.tagName, src;
|
|
var canvasId = this._canvasId;
|
|
|
|
// Get the URL of the image.
|
|
if (tagName) {
|
|
if (tagName.toLowerCase() === "img") {
|
|
src = image.getAttribute("src", 2);
|
|
}
|
|
} else if (image.src) {
|
|
src = image.src;
|
|
}
|
|
|
|
// Do nothing in the following cases:
|
|
// - The first argument is neither an img element nor an object
|
|
// with a src property,
|
|
// - The image has been already cached.
|
|
if (!src || images[canvasId][src]) {
|
|
return;
|
|
}
|
|
|
|
// Store the objects.
|
|
if (onload || onerror) {
|
|
callbacks[canvasId][src] = [image, onload, onerror];
|
|
}
|
|
|
|
// Load the image without drawing.
|
|
this._queue.push(properties.drawImage, 1, encodeXML(src));
|
|
|
|
// Execute the command immediately if possible.
|
|
if (isReady[canvasId]) {
|
|
this._executeCommand();
|
|
++lock[canvasId];
|
|
images[canvasId][src] = true;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* private methods
|
|
*/
|
|
|
|
_initialize: function() {
|
|
// compositing
|
|
this.globalAlpha = this._globalAlpha = 1.0;
|
|
this.globalCompositeOperation = this._globalCompositeOperation = "source-over";
|
|
|
|
// colors and styles
|
|
this.strokeStyle = this._strokeStyle = "#000000";
|
|
this.fillStyle = this._fillStyle = "#000000";
|
|
|
|
// line caps/joins
|
|
this.lineWidth = this._lineWidth = 1.0;
|
|
this.lineCap = this._lineCap = "butt";
|
|
this.lineJoin = this._lineJoin = "miter";
|
|
this.miterLimit = this._miterLimit = 10.0;
|
|
|
|
// shadows
|
|
this.shadowOffsetX = this._shadowOffsetX = 0;
|
|
this.shadowOffsetY = this._shadowOffsetY = 0;
|
|
this.shadowBlur = this._shadowBlur = 0;
|
|
this.shadowColor = this._shadowColor = "rgba(0, 0, 0, 0.0)";
|
|
|
|
// text
|
|
this.font = this._font = "10px sans-serif";
|
|
this.textAlign = this._textAlign = "start";
|
|
this.textBaseline = this._textBaseline = "alphabetic";
|
|
|
|
// command queue
|
|
this._queue = [];
|
|
|
|
// stack of drawing states
|
|
this._stateStack = [];
|
|
},
|
|
|
|
_flush: function() {
|
|
var queue = this._queue;
|
|
this._queue = [];
|
|
return queue;
|
|
},
|
|
|
|
_executeCommand: function() {
|
|
// execute commands
|
|
var commands = this._flush();
|
|
if (commands.length > 0) {
|
|
return eval(this._swf.CallFunction(
|
|
'<invoke name="executeCommand" returntype="javascript"><arguments><string>'
|
|
+ commands.join("�") + "</string></arguments></invoke>"
|
|
));
|
|
}
|
|
},
|
|
|
|
_resize: function(width, height) {
|
|
// Flush commands in the queue
|
|
this._executeCommand();
|
|
|
|
// Clear back to the initial state
|
|
this._initialize();
|
|
|
|
// Adjust the size of Flash to that of the canvas
|
|
if (width > 0) {
|
|
this._swf.width = width;
|
|
}
|
|
if (height > 0) {
|
|
this._swf.height = height;
|
|
}
|
|
|
|
// Execute a resize command at the start of the next frame
|
|
this._queue.push(properties.resize, width, height);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* CanvasGradient stub
|
|
* @constructor
|
|
*/
|
|
var CanvasGradient = function(ctx) {
|
|
this._ctx = ctx;
|
|
this.id = ctx._gradientPatternId++;
|
|
};
|
|
|
|
CanvasGradient.prototype = {
|
|
addColorStop: function(offset, color) {
|
|
// Throws an INDEX_SIZE_ERR exception if the offset is out of range.
|
|
if (isNaN(offset) || offset < 0 || offset > 1) {
|
|
throwException(INDEX_SIZE_ERR);
|
|
}
|
|
|
|
this._ctx._queue.push(properties.addColorStop, this.id, offset, color);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* CanvasPattern stub
|
|
* @constructor
|
|
*/
|
|
var CanvasPattern = function(ctx) {
|
|
this.id = ctx._gradientPatternId++;
|
|
};
|
|
|
|
/**
|
|
* TextMetrics stub
|
|
* @constructor
|
|
*/
|
|
var TextMetrics = function(width) {
|
|
this.width = width;
|
|
};
|
|
|
|
/**
|
|
* DOMException
|
|
* @constructor
|
|
*/
|
|
var DOMException = function(code) {
|
|
this.code = code;
|
|
this.message = DOMExceptionNames[code];
|
|
};
|
|
|
|
DOMException.prototype = new Error;
|
|
|
|
var DOMExceptionNames = {
|
|
1: "INDEX_SIZE_ERR",
|
|
9: "NOT_SUPPORTED_ERR",
|
|
11: "INVALID_STATE_ERR",
|
|
12: "SYNTAX_ERR",
|
|
17: "TYPE_MISMATCH_ERR",
|
|
18: "SECURITY_ERR"
|
|
};
|
|
|
|
/*
|
|
* Event handlers
|
|
*/
|
|
|
|
function onReadyStateChange() {
|
|
if (document.readyState === "complete") {
|
|
document.detachEvent(ON_READY_STATE_CHANGE, onReadyStateChange);
|
|
|
|
var canvases = document.getElementsByTagName(CANVAS);
|
|
for (var i = 0, n = canvases.length; i < n; ++i) {
|
|
FlashCanvas.initElement(canvases[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function onFocus() {
|
|
// forward the event to the parent
|
|
var swf = event.srcElement, canvas = swf.parentNode;
|
|
swf.blur();
|
|
canvas.focus();
|
|
}
|
|
|
|
function onPropertyChange() {
|
|
var prop = event.propertyName;
|
|
if (prop === "width" || prop === "height") {
|
|
var canvas = event.srcElement;
|
|
var value = canvas[prop];
|
|
var number = parseInt(value, 10);
|
|
|
|
if (isNaN(number) || number < 0) {
|
|
number = (prop === "width") ? 300 : 150;
|
|
}
|
|
|
|
if (value === number) {
|
|
canvas.style[prop] = number + "px";
|
|
canvas.getContext("2d")._resize(canvas.width, canvas.height);
|
|
} else {
|
|
canvas[prop] = number;
|
|
}
|
|
}
|
|
}
|
|
|
|
function onUnload() {
|
|
window.detachEvent(ON_UNLOAD, onUnload);
|
|
|
|
for (var canvasId in canvases) {
|
|
var canvas = canvases[canvasId], swf = canvas.firstChild, prop;
|
|
|
|
// clean up the references of swf.executeCommand and swf.resize
|
|
for (prop in swf) {
|
|
if (typeof swf[prop] === "function") {
|
|
swf[prop] = NULL;
|
|
}
|
|
}
|
|
|
|
// clean up the references of canvas.getContext and canvas.toDataURL
|
|
for (prop in canvas) {
|
|
if (typeof canvas[prop] === "function") {
|
|
canvas[prop] = NULL;
|
|
}
|
|
}
|
|
|
|
// remove event listeners
|
|
swf.detachEvent(ON_FOCUS, onFocus);
|
|
canvas.detachEvent(ON_PROPERTY_CHANGE, onPropertyChange);
|
|
}
|
|
|
|
// delete exported symbols
|
|
window[CANVAS_RENDERING_CONTEXT_2D] = NULL;
|
|
window[CANVAS_GRADIENT] = NULL;
|
|
window[CANVAS_PATTERN] = NULL;
|
|
window[FLASH_CANVAS] = NULL;
|
|
window[G_VML_CANVAS_MANAGER] = NULL;
|
|
}
|
|
|
|
/*
|
|
* FlashCanvas API
|
|
*/
|
|
|
|
var FlashCanvas = {
|
|
initElement: function(canvas) {
|
|
// Check whether the initialization is required or not.
|
|
if (canvas.getContext) {
|
|
return canvas;
|
|
}
|
|
|
|
// initialize lock
|
|
var canvasId = getUniqueId();
|
|
var objectId = OBJECT_ID_PREFIX + canvasId;
|
|
isReady[canvasId] = false;
|
|
images[canvasId] = {};
|
|
lock[canvasId] = 1;
|
|
callbacks[canvasId] = {};
|
|
|
|
// Set the width and height attributes.
|
|
setCanvasSize(canvas);
|
|
|
|
// embed swf and SPAN element
|
|
canvas.innerHTML =
|
|
'<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"' +
|
|
' codebase="' + location.protocol + '//fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0"' +
|
|
' width="100%" height="100%" id="' + objectId + '">' +
|
|
'<param name="allowScriptAccess" value="always">' +
|
|
'<param name="flashvars" value="id=' + objectId + '">' +
|
|
'<param name="wmode" value="transparent">' +
|
|
'</object>' +
|
|
'<span style="margin:0;padding:0;border:0;display:inline-block;position:static;height:1em;overflow:visible;white-space:nowrap">' +
|
|
'</span>';
|
|
|
|
canvases[canvasId] = canvas;
|
|
var swf = canvas.firstChild;
|
|
spans[canvasId] = canvas.lastChild;
|
|
|
|
// Check whether the canvas element is in the DOM tree
|
|
var documentContains = document.body.contains;
|
|
if (documentContains(canvas)) {
|
|
// Load swf file immediately
|
|
swf["movie"] = SWF_URL;
|
|
} else {
|
|
// Wait until the element is added to the DOM tree
|
|
var intervalId = setInterval(function() {
|
|
if (documentContains(canvas)) {
|
|
clearInterval(intervalId);
|
|
swf["movie"] = SWF_URL;
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
// If the browser is IE6 or in quirks mode
|
|
if (document.compatMode === "BackCompat" || !window.XMLHttpRequest) {
|
|
spans[canvasId].style.overflow = "hidden";
|
|
}
|
|
|
|
// initialize context
|
|
var ctx = new CanvasRenderingContext2D(canvas, swf);
|
|
|
|
// canvas API
|
|
canvas.getContext = function(contextId) {
|
|
return contextId === "2d" ? ctx : NULL;
|
|
};
|
|
|
|
canvas.toDataURL = function(type, quality) {
|
|
if (("" + type).replace(/[A-Z]+/g, toLowerCase) === "image/jpeg") {
|
|
ctx._queue.push(properties.toDataURL, type,
|
|
typeof quality === "number" ? quality : "");
|
|
} else {
|
|
ctx._queue.push(properties.toDataURL, type);
|
|
}
|
|
return ctx._executeCommand();
|
|
};
|
|
|
|
// add event listener
|
|
swf.attachEvent(ON_FOCUS, onFocus);
|
|
|
|
return canvas;
|
|
},
|
|
|
|
saveImage: function(canvas) {
|
|
var swf = canvas.firstChild;
|
|
swf.saveImage();
|
|
},
|
|
|
|
setOptions: function(options) {
|
|
// TODO: Implement
|
|
},
|
|
|
|
trigger: function(canvasId, type) {
|
|
var canvas = canvases[canvasId];
|
|
canvas.fireEvent("on" + type);
|
|
},
|
|
|
|
unlock: function(canvasId, url, error) {
|
|
var canvas, swf, width, height;
|
|
var _callback, image, callback;
|
|
|
|
if (lock[canvasId]) {
|
|
--lock[canvasId];
|
|
}
|
|
|
|
// If Flash becomes ready
|
|
if (url === undefined) {
|
|
canvas = canvases[canvasId];
|
|
swf = canvas.firstChild;
|
|
|
|
// Set the width and height attributes of the canvas element.
|
|
setCanvasSize(canvas);
|
|
width = canvas.width;
|
|
height = canvas.height;
|
|
|
|
canvas.style.width = width + "px";
|
|
canvas.style.height = height + "px";
|
|
|
|
// Adjust the size of Flash to that of the canvas
|
|
if (width > 0) {
|
|
swf.width = width;
|
|
}
|
|
if (height > 0) {
|
|
swf.height = height;
|
|
}
|
|
swf.resize(width, height);
|
|
|
|
// Add event listener
|
|
canvas.attachEvent(ON_PROPERTY_CHANGE, onPropertyChange);
|
|
|
|
// ExternalInterface is now ready for use
|
|
isReady[canvasId] = true;
|
|
|
|
// Call the onload event handler
|
|
if (typeof canvas.onload === "function") {
|
|
setTimeout(function() {
|
|
canvas.onload();
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
// If callback functions were defined
|
|
else if (_callback = callbacks[canvasId][url]) {
|
|
image = _callback[0];
|
|
callback = _callback[1 + error];
|
|
delete callbacks[canvasId][url];
|
|
|
|
// Call the onload or onerror callback function.
|
|
if (typeof callback === "function") {
|
|
callback.call(image);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Utility methods
|
|
*/
|
|
|
|
// Get the absolute URL of flashcanvas.js
|
|
function getScriptUrl() {
|
|
var scripts = document.getElementsByTagName("script");
|
|
var script = scripts[scripts.length - 1];
|
|
|
|
// @see http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx
|
|
if (document.documentMode >= 8) {
|
|
return script.src;
|
|
} else {
|
|
return script.getAttribute("src", 4);
|
|
}
|
|
}
|
|
|
|
// Get a unique ID composed of alphanumeric characters.
|
|
function getUniqueId() {
|
|
return Math.random().toString(36).slice(2) || "0";
|
|
}
|
|
|
|
// Escape characters not permitted in XML.
|
|
function encodeXML(str) {
|
|
return ("" + str).replace(/&/g, "&").replace(/</g, "<");
|
|
}
|
|
|
|
function toLowerCase(str) {
|
|
return str.toLowerCase();
|
|
}
|
|
|
|
function throwException(code) {
|
|
throw new DOMException(code);
|
|
}
|
|
|
|
// The width and height attributes of a canvas element must have values that
|
|
// are valid non-negative integers.
|
|
function setCanvasSize(canvas) {
|
|
var width = parseInt(canvas.width, 10);
|
|
var height = parseInt(canvas.height, 10);
|
|
|
|
if (isNaN(width) || width < 0) {
|
|
width = 300;
|
|
}
|
|
if (isNaN(height) || height < 0) {
|
|
height = 150;
|
|
}
|
|
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
}
|
|
|
|
/*
|
|
* initialization
|
|
*/
|
|
|
|
// IE HTML5 shiv
|
|
document.createElement(CANVAS);
|
|
|
|
// setup default CSS
|
|
document.createStyleSheet().cssText =
|
|
CANVAS + "{display:inline-block;overflow:hidden;width:300px;height:150px}";
|
|
|
|
// initialize canvas elements
|
|
if (document.readyState === "complete") {
|
|
onReadyStateChange();
|
|
} else {
|
|
document.attachEvent(ON_READY_STATE_CHANGE, onReadyStateChange);
|
|
}
|
|
|
|
// prevent IE6 memory leaks
|
|
window.attachEvent(ON_UNLOAD, onUnload);
|
|
|
|
// preload SWF file if it's in the same domain
|
|
if (SWF_URL.indexOf(location.protocol + "//" + location.host + "/") === 0) {
|
|
var req = new ActiveXObject("Microsoft.XMLHTTP");
|
|
req.open("GET", SWF_URL, false);
|
|
req.send(NULL);
|
|
}
|
|
|
|
/*
|
|
* public API
|
|
*/
|
|
|
|
window[CANVAS_RENDERING_CONTEXT_2D] = CanvasRenderingContext2D;
|
|
window[CANVAS_GRADIENT] = CanvasGradient;
|
|
window[CANVAS_PATTERN] = CanvasPattern;
|
|
window[FLASH_CANVAS] = FlashCanvas;
|
|
|
|
// ExplorerCanvas-compatible APIs for convenience
|
|
window[G_VML_CANVAS_MANAGER] = {
|
|
init: function(){},
|
|
init_: function(){},
|
|
initElement: FlashCanvas.initElement
|
|
};
|
|
|
|
// Prevent Closure Compiler from removing the function.
|
|
keep = [
|
|
CanvasRenderingContext2D.measureText,
|
|
CanvasRenderingContext2D.loadImage
|
|
];
|
|
|
|
})(window, document);
|
|
|
|
} |