/* * fabric.js Controls Extension * for fabric.js current build * Simon Kunz 09.02.2016 for pixolith * Licensed under the MIT license. */ ( function( global ) { 'use strict'; var fabric = global.fabric || ( global.fabric = {} ), extCompat = '1.6', isVML = function() { return typeof G_vmlCanvasManager !== 'undefined'; }, degreesToRadians = fabric.util.degreesToRadians, cursorOffset = { mt: 0, // n tr: 1, // ne mr: 2, // e br: 3, // se mb: 4, // s bl: 5, // sw ml: 6, // w tl: 7 // nw }; if ( global.fabric.version.indexOf( extCompat ) === -1 ) { console.warn( 'this extension might not be fully compatible with your version ' + 'of fabric.js (' + global.fabric.version + ').' + 'Consider using the latest compatible build of fabric.js' + extCompat ); } fabric.util.object.extend( fabric.Object.prototype, { /** * When true, image icons are loaded via the drawImage method * @type Boolean * @default false */ useCustomIcons: false, /** * Sets a background-color for drawImage operations with transparency * @type string * @default transparent */ cornerBackgroundColor: 'transparent', /** * Sets the shape of the background for drawImage operations with transparency * @type string * @default rect */ cornerShape: '', /** * Inner Padding between Shape Background and drawn Image * @type int * @default rect */ cornerPadding: 0, /** * Set a custom corner icon * @param {Object} obj settings and icon url. * @param callback function */ customiseCornerIcons: function( obj, callback ) { var setting; for ( setting in obj ) { if ( obj.hasOwnProperty( setting ) ) { if ( obj[ setting ].cornerShape !== undefined ) { this.cornerShape = obj[ setting ].cornerShape; } if ( obj[ setting ].cornerBackgroundColor !== undefined ) { this.cornerBackgroundColor = obj[ setting ].cornerBackgroundColor; } if ( obj[ setting ].borderColor !== undefined ) { this.borderColor = obj[ setting ].borderColor; } if ( obj[ setting ].cornerSize !== undefined ) { this.cornerSize = obj[ setting ].cornerSize; } if ( obj[ setting ].cornerPadding !== undefined ) { this.cornerPadding = obj[ setting ].cornerPadding; } if ( obj[ setting ].icon !== undefined ) { this.useCustomIcons = true; this.loadIcon( setting, obj[ setting ].icon, function() { if ( callback && typeof( callback ) === 'function' ) { callback(); } } ); } } } }, /** * loads the icon image as an image src. * @param {Object} corner to load an icon. * @param iconUrl as a string. * @param callback function. */ loadIcon: function( corner, iconUrl, callback ) { var self = this, icon = new Image(); icon.onload = function() { self[ corner + 'Icon' ] = this; if ( callback && typeof( callback ) === 'function' ) { callback(); } }; icon.onerror = function() { fabric.warn( this.src + ' icon is not an image' ); }; if ( iconUrl.indexOf( 'http' ) > -1 ) { icon.crossOrigin = 'Anonymous'; } icon.src = iconUrl; }, /** * copy of the setter method for our american friends. * @param {Object} obj containing corner icon urls and settings. */ customizeCornerIcons: function( obj ) { this.customiseCornerIcons( obj ); }, /** * Draws corners of an object's bounding box. * Requires public properties: width, height * Requires public options: cornerSize, padding * @param {CanvasRenderingContext2D} ctx Context to draw on * @return {fabric.Object} thisArg * @chainable */ drawControls: function( ctx ) { if ( !this.hasControls ) { return this; } var wh = this._calculateCurrentDimensions( true ), width = wh.x, height = wh.y, scaleOffset = this.cornerSize, left = -( width + scaleOffset ) / 2, top = -( height + scaleOffset ) / 2, methodName; if ( !this.useCustomIcons ) { ctx.lineWidth = 1; ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; ctx.strokeStyle = ctx.fillStyle = this.cornerColor; methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; } else { methodName = 'drawImage'; } ctx.save(); // top-left this._drawControl( 'tl', ctx, methodName, left, top, this.tlIcon ); // top-right this._drawControl( 'tr', ctx, methodName, left + width, top, this.trIcon ); // bottom-left this._drawControl( 'bl', ctx, methodName, left, top + height, this.blIcon ); // bottom-right this._drawControl( 'br', ctx, methodName, left + width, top + height, this.brIcon ); if ( !this.get( 'lockUniScaling' ) ) { // middle-top this._drawControl( 'mt', ctx, methodName, left + width / 2, top, this.mtIcon ); // middle-bottom this._drawControl( 'mb', ctx, methodName, left + width / 2, top + height, this.mbIcon ); // middle-right this._drawControl( 'mr', ctx, methodName, left + width, top + height / 2, this.mrIcon ); // middle-left this._drawControl( 'ml', ctx, methodName, left, top + height / 2, this.mlIcon ); } // middle-top-rotate if ( this.hasRotatingPoint ) { this._drawControl( 'mtr', ctx, methodName, left + width / 2, top - this.rotatingPointOffset, this.mtrIcon ); } ctx.restore(); return this; }, /** Draw controls either with background-shape and color (transparency) or plain image (modified core method) * @private * {string} icon url of the control */ _drawControl: function( control, ctx, methodName, left, top, icon ) { if ( !this.isControlVisible( control ) ) { return; } var size = this.cornerSize, cornerBG = this.cornerBackgroundColor || 'black', cornerShape = this.cornerShape || 'rect', cornerPadding = this.cornerPadding || 10 ; if ( this.useCustomIcons ) { if ( cornerShape ) { ctx.globalAlpha = 1; ctx.fillStyle = cornerBG; switch ( cornerShape ) { case 'rect': ctx.fillRect( left, top, size, size ); if ( icon !== undefined ) { ctx[ methodName ]( icon, left + cornerPadding / 2, top + cornerPadding / 2, size - cornerPadding, size - cornerPadding ); } break; case 'circle': ctx.beginPath(); ctx.arc( left + size / 2, top + size / 2, size / 2, 0, 2 * Math.PI ); ctx.fill(); ctx.closePath(); if ( icon !== undefined ) { ctx[ methodName ]( icon, left + cornerPadding / 2, top + cornerPadding / 2, size - cornerPadding, size - cornerPadding ); } break; } } else { if ( icon !== undefined ) { ctx[ methodName ]( icon, left, top, size, size ); } } } else { isVML() || this.transparentCorners || ctx.clearRect( left, top, size, size ); ctx[ methodName ]( left, top, size, size ); } } } ); fabric.util.object.extend( fabric.Canvas.prototype, { /** * When true, actions can be overwritten * @type Boolean * @default false */ overwriteActions: false, /** * When true, cursors are fixed * @type Boolean * @default false */ fixedCursors: false, /** * setter Method for actions and cursors. * @param {Object} obj containing corner action and cursor url/type. */ customiseControls: function( obj ) { var setting; for ( setting in obj ) { if ( obj.hasOwnProperty( setting ) ) { if ( obj[ setting ].action !== undefined ) { this.overwriteActions = true; this.setCustomAction( setting, obj[ setting ].action ); } if ( obj[ setting ].cursor !== undefined ) { this.fixedCursors = true; this.setCustomCursor( setting, obj[ setting ].cursor ); } } } }, /** * loads the icon image as an image src. * @param {Object} corner to load an icon. * @param action as a string. */ setCustomAction: function( corner, action ) { this[ corner + 'Action' ] = action ; }, /** * loads the icon image as an image src. * @param {Object} corner to load an icon. * @param cursorUrl as a string. */ setCustomCursor: function( corner, cursorUrl ) { this[ corner + 'cursorIcon' ] = cursorUrl ; }, /** * copy of the setter method for our american friends. * @param {Object} obj containing corner action and cursor url/type. */ customizeControls: function( obj ) { this.customiseControls( obj ); }, /** * @private */ _getActionFromCorner: function( target, corner, e ) { if ( !corner ) { return 'drag'; } if ( corner ) { if ( this[ corner + 'Action' ] && this.overwriteActions ) { switch ( corner ) { case 'mtr': return this[ corner + 'Action' ] || 'rotate'; case 'ml': case 'mr': if ( e.shiftKey ) { return e.shiftKey ? 'skewY' : 'scaleX'; } return this[ corner + 'Action' ]; case 'mt': case 'mb': if ( e.shiftKey ) { return e.shiftKey ? 'skewY' : 'scaleY'; } return this[ corner + 'Action' ]; default: return this[ corner + 'Action' ] || 'scale'; } } else { switch ( corner ) { case 'mtr': return 'rotate'; case 'ml': case 'mr': return e.shiftKey ? 'skewY' : 'scaleX'; case 'mt': case 'mb': return e.shiftKey ? 'skewX' : 'scaleY'; default: return 'scale'; } } } }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _setupCurrentTransform: function( e, target ) { if ( !target ) { return; } var pointer = this.getPointer( e ), corner = target._findTargetCorner( this.getPointer( e, true ) ), action = this._getActionFromCorner( target, corner, e ), origin = this._getOriginFromCorner( target, corner ), _this = this; if ( typeof action === 'function' ) { action.call( _this, e, target ); } this._currentTransform = { target: target, action: action, corner: corner, scaleX: target.scaleX, scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, originY: origin.y, ex: pointer.x, ey: pointer.y, lastX: pointer.x, lastY: pointer.y, left: target.left, top: target.top, theta: degreesToRadians( target.angle ), width: target.width * target.scaleX, mouseXSign: 1, mouseYSign: 1, shiftKey: e.shiftKey, altKey: e.altKey }; this._currentTransform.original = { left: target.left, top: target.top, scaleX: target.scaleX, scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, originX: origin.x, originY: origin.y }; if ( action === 'remove' ) { this._removeAction( e, target ); } if ( action === 'moveUp' ) { this._moveLayerUpAction( e, target ); } if ( action === 'moveDown' ) { this._moveLayerDownAction( e, target ); } if ( typeof action === 'object' ) { if ( Object.keys( action )[ 0 ] === 'rotateByDegrees' ) { this._rotateByDegrees( e, target, action.rotateByDegrees ); } } this._resetCurrentTransform( e ); }, /** * Custom remove object action * @private * @param {Event} e Event object * @param {fabric.Object} target */ _removeAction: function( e, target ) { var _this = this; if ( this.getActiveGroup() && this.getActiveGroup() !== 'undefined' ) { this.getActiveGroup().forEachObject( function( o ) { o.remove(); } ); this.discardActiveGroup(); // as of fabric 1.6.3 necessary for reasons.. setTimeout( function() { _this.deactivateAll(); }, 0 ); } else { target.remove(); setTimeout( function() { _this.deactivateAll(); }, 0 ); } }, /** * Custom move up object action * @private * @param {Event} e Event object * @param {fabric.Object} target */ _moveLayerUpAction: function( e, target ) { if ( this.getActiveGroup() && this.getActiveGroup() !== 'undefined' ) { this.getActiveGroup().forEachObject( function( o ) { o.bringForward(); } ); } else { target.bringForward(); } }, /** * Custom move down object action * @private * @param {Event} e Event object * @param {fabric.Object} target */ _moveLayerDownAction: function( e, target ) { if ( this.getActiveGroup() && this.getActiveGroup() !== 'undefined' ) { this.getActiveGroup().forEachObject( function( o ) { o.sendBackwards(); } ); } else { target.sendBackwards(); } }, /** * Custom move down object action * @private * @param {Event} e Event object * @param {fabric.Object} target * @param {Integer} value of rotation */ _rotateByDegrees: function( e, target, value ) { var angle = parseInt( target.getAngle() ) + value, needsOriginRestore = false; if ( ( target.originX !== 'center' || target.originY !== 'center' ) && target.centeredRotation ) { this._setOriginToCenter( target ); needsOriginRestore = true; } angle = angle > 360 ? angle - 360 : angle; if ( this.getActiveGroup() && this.getActiveGroup() !== 'undefined' ) { this.getActiveGroup().forEachObject( function( obj ) { obj .setAngle( angle ) .setCoords(); } ); } else { target .setAngle( angle ) .setCoords(); } if ( needsOriginRestore ) { this._setCenterToOrigin( target ); } this.renderAll(); }, _setOriginToCenter: function( target ) { target._originalOriginX = target.originX; target._originalOriginY = target.originY; var center = target.getCenterPoint(); target.set( { originX: 'center', originY: 'center', left: center.x, top: center.y } ); }, _setCenterToOrigin: function( target ) { var originPoint = target.translateToOriginPoint( target.getCenterPoint(), target._originalOriginX, target._originalOriginY ); target.set( { originX: target._originalOriginX, originY: target._originalOriginY, left: originPoint.x, top: originPoint.y } ); }, /** * Sets either the standard behaviour cursors or if fixedCursors is true, tries to set a custom cursor * either by using an icon or a build-in cursor. Cursor icon extensions are matched with a regular expression. * @private * {string} corner name * {target} event handler of the hovered corner */ _setCornerCursor: function( corner, target, e ) { var iconUrlPattern = /\.(?:jpe?g|png|gif|jpg|jpeg|svg)$/; if ( this.fixedCursors && this[ corner + 'cursorIcon' ] ) { if ( iconUrlPattern.test( this[ corner + 'cursorIcon' ] ) ) { this.setCursor( 'url(' + this[ corner + 'cursorIcon' ] + '), auto' ); } else { if ( this[ corner + 'cursorIcon' ] === 'resize' ) { this.setCursor( this._getRotatedCornerCursor( corner, target, e ) ); } else { this.setCursor( this[ corner + 'cursorIcon' ] ); } } } else { if ( corner in cursorOffset ) { this.setCursor( this._getRotatedCornerCursor( corner, target, e ) ); } else if ( corner === 'mtr' && target.hasRotatingPoint ) { this.setCursor( this.rotationCursor ); } else { this.setCursor( this.defaultCursor ); return false; } } } } ); } )( typeof exports !== 'undefined' ? exports : this );