/*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 elements) this._oElement.parentNode.style[prop] = value + 'px'; this._oConfig[prop] = value; this.calcOffset(); this.renderAll(); return this; }, /** * Method that defines the actions when mouse is released on canvas. * The method resets the currentTransform parameters, store the image corner * position in the image object and render the canvas on top. * @method __onMouseUp * @param {Event} e Event object fired on mouseup * */ __onMouseUp: function (e) { if (this._currentTransform) { var transform = this._currentTransform, target = transform.target; if (target.__scaling) { document.fire('object:scaled', { target: target }); target.__scaling = false; } // determine the new coords everytime the image changes its position for (var i=0, l=this._aObjects.length; i 0) ? 0 : -left), this._groupSelector.ey - ((top > 0) ? 0 : -top), aleft, atop ); this._oContextTop.lineWidth = this.selectionLineWidth; this._oContextTop.strokeStyle = this.selectionBorderColor; this._oContextTop.strokeRect( this._groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), this._groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), aleft, atop ); }, _findSelectedObjects: function (e) { var pointer = getCoords(e), target, targetRegion, group = [], x1 = this._groupSelector.ex, y1 = this._groupSelector.ey, x2 = x1 + this._groupSelector.left, y2 = y1 + this._groupSelector.top, currentObject; var selectionX1Y1 = new Canvas.Point2D(Math.min(x1,x2), Math.min(y1,y2)), selectionX2Y2 = new Canvas.Point2D(Math.max(x1,x2), Math.max(y1,y2)); for (var i=0, l=this._aObjects.length; i 1) { var group = new Canvas.Group(group); this.setActiveGroup(group); group.saveCoords(); document.fire('group:selected', { target: group }); } this.renderAll(); }, /** * Adds an object to canvas and renders canvas * An object should be an instance of (or inherit from) Canvas.Object * @method add * @return {Canvas.Element} thisArg * @chainable */ add: function () { this._aObjects.push.apply(this._aObjects, arguments); this.renderAll(); return this; }, /** * Inserts an object to canvas at specified index and renders canvas. * An object should be an instance of (or inherit from) Canvas.Object * @method insertAt * @param object {Object} Object to insert * @param index {Number} index to insert object at * @return {Canvas.Element} instance */ insertAt: function (object, index) { this._aObjects.splice(index, 0, object); this.renderAll(); return this; }, /** * Returns an array of objects this instance has * @method getObjects * @return {Array} */ getObjects: function () { return this._aObjects; }, /** * Returns topmost canvas context * @method getContext * @return {CanvasRenderingContext2D} */ getContext: function () { return this._oContextTop; }, /** * Clears specified context of canvas element * @method clearContext * @param context {Object} ctx context to clear * @return {Canvas.Element} thisArg * @chainable */ clearContext: clearContext, /** * Clears all contexts of canvas element * @method clear * @return {Canvas.Element} thisArg * @chainable */ clear: function () { this._aObjects.length = 0; this.clearContext(this._oContextTop); this.clearContext(this._oContextContainer); this.renderAll(); return this; }, /** * Renders both the top canvas and the secondary container canvas. * @method renderAll * @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas * @return {Canvas.Element} instance * @chainable */ renderAll: function (allOnTop) { // this sucks, but we can't use `getWidth`/`getHeight` here for perf. reasons var w = this._oConfig.width, h = this._oConfig.height; // when allOnTop is true all images are rendered in the top canvas. // This is used for actions like toDataUrl that needs to take some actions on a unique canvas. var containerCanvas = allOnTop ? this._oContextTop : this._oContextContainer; this.clearContext(this._oContextTop); if (containerCanvas !== this._oContextTop) { this.clearContext(containerCanvas); } if (allOnTop) { if (!CAN_SET_TRANSPARENT_FILL && this.backgroundColor === 'transparent') { var skip = true; } if (!skip) { containerCanvas.fillStyle = this.backgroundColor; } containerCanvas.fillRect(0, 0, w, h); } var length = this._aObjects.length, activeGroup = this.getActiveGroup(); if (length) { for (var i=0; i 1) { var object = new Canvas.PathGroup(elements, obj); } else { var object = elements[0]; } object.setSourcePath(path); // copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.) // skip this step if an object is a PathGroup, since we already passed it options object before if (!(object instanceof Canvas.PathGroup)) { Object.extend(object, obj); if (typeof obj.angle !== 'undefined') { object.setAngle(obj.angle); } } onObjectLoaded(object, index); }); } } }, this); } catch(e) { console.log(e.message); } }, /** * Loads an image from URL * @method loadImageFromURL * @param url {String} url of image to load * @param callback {Function} calback, invoked when image is loaded */ loadImageFromURL: (function () { var imgCache = { }; return function (url, callback) { // check cache first var _this = this; function checkIfLoaded() { var imgEl = document.getElementById(imgCache[url]); if (imgEl.width && imgEl.height) { callback(new Canvas.Image(imgEl)); } else { setTimeout(checkIfLoaded, 50); } } // get by id from cache if (imgCache[url]) { // id can be cached but image might still not be loaded, so we poll here checkIfLoaded(); } // else append a new image element else { var imgEl = new Image(); imgEl.onload = function () { imgEl.onload = null; _this._resizeImageToFit(imgEl); var oImg = new Canvas.Image(imgEl); callback(oImg); }; imgEl.className = 'canvas-img-clone'; imgEl.src = url; if (this.shouldCacheImages) { imgCache[url] = Element.identify(imgEl); } document.body.appendChild(imgEl); } } })(), loadSVGFromURL: function (url, callback) { var _this = this; url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').strip(); this.cache.has(url, function (hasUrl) { if (hasUrl) { _this.cache.get(url, function (value) { var enlivedRecord = _this._enlivenCachedObject(value); callback(enlivedRecord.objects, enlivedRecord.options); }); } else { new Ajax.Request(url, { method: 'get', onComplete: onComplete, onFailure: onFailure }); } }); function onComplete(r) { var xml = r.responseXML; if (!xml) return; var doc = xml.documentElement; if (!doc) return; Canvas.parseSVGDocument(doc, function (results, options) { _this.cache.set(url, { objects: results.invoke('toObject'), options: options }); callback(results, options); }); } function onFailure() { console.log('ERROR!'); } }, _enlivenCachedObject: function (cachedObject) { var objects = cachedObject.objects; var options = cachedObject.options; objects = objects.map(function (o) { return Canvas[o.type.capitalize()].fromObject(o); }); return ({ objects: objects, options: options }); }, /** * Removes an object from canvas and returns it * @method remove * @param object {Object} Object to remove * @return {Object} removed object */ remove: function (object) { Canvas.util.removeFromArray(this._aObjects, object); this.renderAll(); return object; }, /** * Same as `remove` but animated * @method fxRemove * @param {Canvas.Object} object Object to remove * @param {Function} callback callback, invoked on effect completion * @return {Canvas.Element} thisArg * @chainable */ fxRemove: function (object, callback) { var _this = this; object.fxRemove({ onChange: this.renderAll.bind(this), onComplete: function () { _this.remove(object); if (typeof callback === 'function') { callback(); } } }); return this; }, /** * Moves an object to the bottom of the stack * @method sendToBack * @param object {Canvas.Object} Object to send to back * @return {Canvas.Element} thisArg * @chainable */ sendToBack: function (object) { Canvas.util.removeFromArray(this._aObjects, object); this._aObjects.unshift(object); return this.renderAll(); }, /** * Moves an object to the top of the stack * @method bringToFront * @param object {Canvas.Object} Object to send * @return {Canvas.Element} thisArg * @chainable */ bringToFront: function (object) { Canvas.util.removeFromArray(this._aObjects, object); this._aObjects.push(object); return this.renderAll(); }, /** * Moves an object one level down in stack * @method sendBackwards * @param object {Canvas.Object} Object to send * @return {Canvas.Element} thisArg * @chainable */ sendBackwards: function (object) { var idx = this._aObjects.indexOf(object), nextIntersectingIdx = idx; // if object is not on the bottom of stack if (idx !== 0) { // traverse down the stack looking for the nearest intersecting object for (var i=idx-1; i>=0; --i) { if (object.intersectsWithObject(this._aObjects[i])) { nextIntersectingIdx = i; break; } } Canvas.util.removeFromArray(this._aObjects, object); this._aObjects.splice(nextIntersectingIdx, 0, object); } return this.renderAll(); }, /** * Moves an object one level up in stack * @method sendForward * @param object {Canvas.Object} Object to send * @return {Canvas.Element} thisArg * @chainable */ bringForward: function (object) { var objects = this.getObjects(), idx = objects.indexOf(object), nextIntersectingIdx = idx; // if object is not on top of stack (last item in an array) if (idx !== objects.length-1) { // traverse up the stack looking for the nearest intersecting object for (var i=idx+1, l=this._aObjects.length; i'; }; Object.extend(Canvas.Element, { /** * @property EMPTY_JSON */ EMPTY_JSON: '{"objects": [], "background": "white"}', /** * @static * @method toGrayscale * @param {HTMLCanvasElement} canvasEl */ toGrayscale: function (canvasEl) { var context = canvasEl.getContext('2d'), imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), data = imageData.data, iLen = imageData.width, jLen = imageData.height, index, average; for (i = 0; i < iLen; i++) { for (j = 0; j < jLen; j++) { index = (i * 4) * jLen + (j * 4); average = (data[index] + data[index + 1] + data[index + 2]) / 3; data[index] = average; data[index + 1] = average; data[index + 2] = average; } } context.putImageData(imageData, 0, 0); }, /** * Provides a way to check support of some of the canvas methods * (either those of HTMLCanvasElement itself, or rendering context) * @method supports * @param methodName {String} method to check support for * @return {Boolean | null} `true` if method is supported (or at least exists), * `null` if canvas element or context can not be initialized */ supports: function (methodName) { var el = document.createElement('canvas'); if (!el || !el.getContext) { return null; } var ctx = el.getContext('2d'); if (!ctx) { return null; } switch (methodName) { case 'getImageData': return typeof ctx.getImageData !== 'undefined'; case 'toDataURL': return typeof el.toDataURL !== 'undefined'; default: return null; } } }); })();