/*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
/*global Image: false, APE: false, $: false */
(function () {
var global = this,
window = global.window,
document = window.document,
Canvas = global.Canvas || (global.Canvas = { });
if (Canvas.Element) {
return;
}
var CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'),
FX_DURATION = 500,
STROKE_OFFSET = 0.5,
FX_TRANSITION = 'decel',
getCoords = APE.dom.Event.getCoords,
cursorMap = {
'tr': 'ne-resize',
'br': 'se-resize',
'bl': 'sw-resize',
'tl': 'nw-resize',
'ml': 'w-resize',
'mt': 'n-resize',
'mr': 'e-resize',
'mb': 's-resize'
};
// WebKit is about 10x faster at clearing canvas with `canvasEl.width = canvasEl.width` rather than `context.clearRect`
// We feature-test performance of both methods to determine a winner
var fastestClearingMethod = (function () {
var el = document.createElement('canvas'),
t, t1, t2, i,
numIterations = 200,
canvasLength = 300;
el.width = el.height = canvasLength;
if (!el.getContext) {
return;
}
var ctx = el.getContext('2d');
if (!ctx) {
return;
}
t = new Date();
for (i = numIterations; i--; ) {
ctx.clearRect(0, 0, canvasLength, canvasLength);
}
t1 = new Date() - t;
t = new Date();
for (i = numIterations; i--; ) {
el.width = el.height;
}
t2 = new Date() - t;
if (t2 < t1) {
return 'width';
}
})();
function clearContext(ctx) {
// this sucks, but we can't use `getWidth`/`getHeight` here for perf. reasons
ctx.clearRect(0, 0, this._oConfig.width, this._oConfig.height);
return this;
}
// nightly webkit has some rendering artifacts when using this clearing method, so disable it for now
/*
if (fastestClearingMethod === 'width') {
clearContext = function (ctx) {
ctx.canvas.width = ctx.canvas.width;
return this;
}
}
*/
var CAN_SET_TRANSPARENT_FILL = (function () {
// FF2.0 (and maybe other equivalents) throw error
var canvasEl = document.createElement('canvas');
if (!canvasEl || !canvasEl.getContext) {
return;
}
var context = canvasEl.getContext('2d');
if (!context) {
return;
}
try {
context.fillStyle = 'transparent';
return true;
}
catch(err) { }
return false;
})();
/**
* @class Canvas.Element
* @constructor
* @param {HTMLElement | String} el Container element for the canvas.
*/
Canvas.Element = function (el, oConfig) {
/**
* The object literal containing mouse position if clicked in an empty area (no image)
* @property _groupSelector
* @type object
*/
this._groupSelector = null;
/**
* The array literal containing all objects on canvas
* @property _aObjects
* @type array
*/
this._aObjects = [];
/**
* The element that references the canvas interface implementation
* @property _oContext
* @type object
*/
this._oContext = null;
/**
* The main element that contains the canvas
* @property _oElement
* @type object
*/
this._oElement = null;
/**
* The object literal containing the current x,y params of the transformation
* @property _currentTransform
* @type object
*/
this._currentTransform = null;
/**
* References instance of Canvas.Group - when multiple objects are selected
* @property _activeGroup
* @type object
*/
this._activeGroup = null;
/**
* An object containing config parameters
* @property _oConfig
* @type object
*/
this._oConfig = {
width: 300,
height: 150
};
oConfig = oConfig || { };
this._initElement(el);
this._initConfig(oConfig);
if (oConfig.overlayImage) {
this.setOverlayImage(oConfig.overlayImage);
}
if (oConfig.afterRender) {
this.afterRender = oConfig.afterRender;
}
this._createCanvasBackground();
this._createCanvasContainer();
this._initEvents();
this.calcOffset();
};
Object.extend(Canvas.Element.prototype, {
selectionColor: 'rgba(100,100,255,0.3)', // blue
selectionBorderColor: 'rgba(255,255,255,0.3)', // white
selectionLineWidth: 1,
backgroundColor: 'rgba(255,255,255,1)', // white
includeDefaultValues: true,
shouldCacheImages: false,
CANVAS_WIDTH: 600,
CANVAS_HEIGHT: 600,
CANVAS_PRINT_WIDTH: 3000,
CANVAS_PRINT_HEIGHT: 3000,
onBeforeScaleRotate: function () {
/* NOOP */
},
/**
* Calculates canvas element offset relative to the document
* This method is also attached as "resize" event handler of window
* @method calcOffset
* @return {Canvas.Element} instance
* @chainable
*/
calcOffset: function () {
this._offset = Element.cumulativeOffset(this.getElement());
return this;
},
/**
* @method setOverlayImage
* @param {String} url url of an image to set background to
* @param {Function} callback callback to invoke when image is loaded and set as an overlay one
* @return {Canvas.Element} thisArg
* @chainable
*/
// TODO (kangax): test callback
setOverlayImage: function (url, callback) {
if (url) {
var _this = this, img = new Image();
img.onload = function () {
_this.overlayImage = img;
if (callback) {
callback();
}
img = img.onload = null;
};
img.src = url;
}
return this;
},
/**
* canvas class's initialization method. This method is automatically
* called by constructor, and sets up all DOM references for
* pre-existing markup, and creates required markup if it is not
* already present.
* @method _initElement
* @param canvasEl {HTMLElement|String} canvasEl canvas element
*
*/
_initElement: function (canvasEl) {
if ($(canvasEl)) {
this._oElement = $(canvasEl);
}
else {
this._oElement = new Element('canvas');
}
if (typeof this._oElement.getContext === 'undefined') {
G_vmlCanvasManager.initElement(this._oElement);
}
if (typeof this._oElement.getContext === 'undefined') {
throw CANVAS_INIT_ERROR;
}
if (!(this._oContextTop = this._oElement.getContext('2d'))) {
throw CANVAS_INIT_ERROR;
}
var width = this._oElement.width || 0,
height = this._oElement.height || 0;
this._initWrapperElement(width, height);
this._setElementStyle(width, height);
},
/**
* @private
* @method _initWrapperElement
*/
_initWrapperElement: function (width, height) {
var wrapper = Element.wrap(this.getElement(), 'div', { className: 'canvas_container' });
wrapper.setStyle({
width: width + 'px',
height: height + 'px'
});
this._makeElementUnselectable(wrapper);
this.wrapper = wrapper;
},
/**
* @private
* @method _setElementStyle
*/
_setElementStyle: function (width, height) {
this.getElement().setStyle({
position: 'absolute',
width: width + 'px',
height: height + 'px',
left: 0,
top: 0
});
},
/**
* For now we use an object literal without methods to store the config params
* @method _initConfig
* @param oConfig {Object} userConfig The configuration Object literal
* containing the configuration that should be set for this module.
* See configuration documentation for more details.
*/
_initConfig: function (oConfig) {
Object.extend(this._oConfig, oConfig || { });
this._oConfig.width = parseInt(this._oElement.width, 10) || 0;
this._oConfig.height = parseInt(this._oElement.height, 10) || 0;
this._oElement.style.width = this._oConfig.width + 'px';
this._oElement.style.height = this._oConfig.height + 'px';
},
/**
* Adds main mouse listeners to the whole canvas
* @method _initEvents
* @private
* See configuration documentation for more details.
*/
_initEvents: function () {
var _this = this;
this._onMouseDown = function (e){ _this.__onMouseDown(e); };
this._onMouseUp = function (e){ _this.__onMouseUp(e); };
this._onMouseMove = function (e){ _this.__onMouseMove(e); };
this._onResize = function (e) { _this.calcOffset() };
Event.observe(this._oElement, 'mousedown', this._onMouseDown);
Event.observe(document, 'mousemove', this._onMouseMove);
Event.observe(document, 'mouseup', this._onMouseUp);
Event.observe(window, 'resize', this._onResize);
},
/**
* Creates canvas elements
* @method _createCanvasElement
* @private
*/
_createCanvasElement: function (className) {
var element = document.createElement('canvas');
if (!element) {
return;
}
element.className = className;
var oContainer = this._oElement.parentNode.insertBefore(element, this._oElement);
oContainer.width = this.getWidth();
oContainer.height = this.getHeight();
oContainer.style.width = this.getWidth() + 'px';
oContainer.style.height = this.getHeight() + 'px';
oContainer.style.position = 'absolute';
oContainer.style.left = 0;
oContainer.style.top = 0;
if (typeof element.getContext === 'undefined') {
// try augmenting element with excanvas' G_vmlCanvasManager
G_vmlCanvasManager.initElement(element);
}
if (typeof element.getContext === 'undefined') {
// if that didn't work, throw error
throw CANVAS_INIT_ERROR;
}
this._makeElementUnselectable(oContainer);
return oContainer;
},
/**
* Creates a secondary canvas to contain all the images are not being translated/rotated/scaled
* @method _createCanvasContainer
*/
_createCanvasContainer: function () {
// this context will contain all images that are not on the top
var canvas = this._createCanvasElement('canvas-container');
this._oContextContainerEl = canvas;
this._oContextContainer = canvas.getContext('2d');
},
/**
* Creates a "background" canvas
* @method _createCanvasBackground
*/
_createCanvasBackground: function () {
// this context will contain the background
var canvas = this._createCanvasElement('canvas-container');
this._oContextBackgroundEl = canvas;
this._oContextBackground = canvas.getContext('2d');
},
/**
* @private
* @method _makeElementUnselectable
*/
_makeElementUnselectable: function (element) {
if ('onselectstart' in element) {
element.onselectstart = Prototype.falseFunction;
}
},
/**
* Returns canvas width
* @method getWidth
* @return {Number}
*/
getWidth: function () {
return this._oConfig.width;
},
/**
* Returns canvas height
* @method getHeight
* @return {Number}
*/
getHeight: function () {
return this._oConfig.height;
},
/**
* @method setWidth
* @param {Number} width value to set width to
* @return {Canvas.Element} instance
* @chainable true
*/
setWidth: function (value) {
return this._setDimension('width', value);
},
/**
* @method setHeight
* @param {Number} height value to set height to
* @return {Canvas.Element} instance
* @chainable true
*/
setHeight: function (value) {
return this._setDimension('height', value);
},
/**
* private helper for setting width/height
* @method _setDimensions
* @private
* @param {String} prop property (width|height)
* @param {Number} value value to set property to
* @return {Canvas.Element} instance
* @chainable true
*/
_setDimension: function (prop, value) {
this._oContextContainerEl[prop] = value;
this._oContextContainerEl.style[prop] = value + 'px';
this._oContextBackgroundEl[prop] = value;
this._oContextBackgroundEl.style[prop] = value + 'px';
this._oElement[prop] = value;
this._oElement.style[prop] = value + 'px';
//
container (parent of all