/* * Licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * */ (function (root, factory) { if ( typeof define === 'function' && define.amd ) { // AMD. Register as an anonymous module. define(['raphael'], function(Raphael) { // Use global variables if the locals are undefined. return factory(Raphael || root.Raphael); }); } else { // RequireJS isn't being used. Assume underscore and backbone are loaded in <script> tags factory(Raphael); } }(this, function(Raphael) { Raphael.fn.freeTransform = function(subject, options, callback) { var ft, i, mapped, timeout, paper = this, bbox = subject.getBBox(true); // Enable method chaining if ( subject.freeTransform ) { return subject.freeTransform; } // Add Array.map if the browser doesn't support it if ( !Array.prototype.hasOwnProperty('map') ) { Array.prototype.map = function(callback, arg) { mapped = []; for ( i in this ) { if ( this.hasOwnProperty(i) ) { mapped[i] = callback.call(arg, this[i], i, this); } } return mapped; }; } // Add Array.indexOf if not builtin if ( !Array.prototype.hasOwnProperty('indexOf') ) { Array.prototype.indexOf = function(obj, start) { for ( i = ( start || 0 ), j = this.length; i < j; i++ ) { if ( this[i] === obj ) { return i; } } return -1; }; } ft = subject.freeTransform = { // Keep track of transformations attrs: { x: bbox.x, y: bbox.y, size: { x: bbox.width, y: bbox.height }, center: { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 }, rotate: 0, scale: { x: 1, y: 1 }, translate: { x: 0, y: 0 }, ratio: 1 }, axes: null, bbox: null, callback: null, items: [], handles: { center: null, x: null, y: null }, offset: { rotate: 0, scale: { x: 1, y: 1 }, translate: { x: 0, y: 0 } }, opts: { animate: false, attrs: { fill: '#fff', stroke: '#000' }, boundary: { x: paper._left || 0, y: paper._top || 0, width: null, height: null }, customCorners: false, // { size: <number>, distance: <number>, corners: [ action: <string>, image: <string> ] } distance: 1.3, drag: true, draw: false, keepRatio: false, range: { rotate: [ -180, 180 ], scale: [ -99999, 99999 ] }, rotate: true, scale: true, snap: { rotate: 0, scale: 0, drag: 0 }, snapDist: { rotate: 0, scale: 0, drag: 7 }, size: 5 }, subject: subject }; /** * Update handles based on the element's transformations */ ft.updateHandles = function() { var corners, cornersDistance, rad, radius, cx, cy, viewBoxRatio, bboxHandleDirection = [ // Allowed x, y scaling directions for bbox handles [ -1, -1 ], [ 1, -1 ], [ 1, 1 ], [ -1, 1 ], [ 0, -1 ], [ 1, 0 ], [ 0, 1 ], [ -1, 0 ] ]; if ( ft.handles.bbox || ft.opts.rotate.indexOf('self') >= 0 ) { cornersDistance = corners = getBBox(0); if ( ft.opts.customCorners && typeof ft.opts.customCorners.distance === 'number' ) { cornersDistance = getBBox(ft.opts.customCorners.distance); } } // Get the element's rotation rad = { x: ( ft.attrs.rotate ) * Math.PI / 180, y: ( ft.attrs.rotate + 90 ) * Math.PI / 180 }; radius = { x: ft.attrs.size.x / 2 * ft.attrs.scale.x, y: ft.attrs.size.y / 2 * ft.attrs.scale.y }; ft.axes.map(function(axis) { if ( ft.handles[axis] ) { cx = ft.attrs.center.x + ft.attrs.translate.x + radius[axis] * ft.opts.distance * Math.cos(rad[axis]), cy = ft.attrs.center.y + ft.attrs.translate.y + radius[axis] * ft.opts.distance * Math.sin(rad[axis]), viewBoxRatio = { x: 1, y: 1 }; // viewBox might be scaled if ( paper._viewBox ) { viewBoxRatio = { x: paper._viewBox[2] / getPaperSize().x, y: paper._viewBox[3] / getPaperSize().y }; } // Keep handle within boundaries if ( ft.opts.boundary ) { cx = Math.max(Math.min(cx, ft.opts.boundary.x + ( ft.opts.boundary.width || getPaperSize().x * viewBoxRatio.x)), ft.opts.boundary.x); cy = Math.max(Math.min(cy, ft.opts.boundary.y + ( ft.opts.boundary.height || getPaperSize().y * viewBoxRatio.y)), ft.opts.boundary.y); } ft.handles[axis].disc.attr({ cx: cx, cy: cy }); ft.handles[axis].line.toFront().attr({ path: [ [ 'M', ft.attrs.center.x + ft.attrs.translate.x, ft.attrs.center.y + ft.attrs.translate.y ], [ 'L', ft.handles[axis].disc.attrs.cx, ft.handles[axis].disc.attrs.cy ] ] }); ft.handles[axis].disc.toFront(); } }); if ( ft.bbox ) { ft.bbox.toFront().attr({ path: [ [ 'M', corners[0].x, corners[0].y ], [ 'L', corners[1].x, corners[1].y ], [ 'L', corners[2].x, corners[2].y ], [ 'L', corners[3].x, corners[3].y ], [ 'L', corners[0].x, corners[0].y ] ] }); if ( ft.handles.bbox ) { ft.handles.bbox.map(function (handle, i) { var cx, cy, j, k; if ( handle.isCorner ) { cx = cornersDistance[i].x; cy = cornersDistance[i].y; } else { j = i % 4; k = ( j + 1 ) % corners.length; cx = ( cornersDistance[j].x + cornersDistance[k].x ) / 2; cy = ( cornersDistance[j].y + cornersDistance[k].y ) / 2; } handle.element.toFront() .attr({ x: cx - ( handle.isCorner ? ( ft.opts.customCorners ? Math.ceil(ft.opts.customCorners.size / 2) : ft.opts.size.bboxCorners ) : ft.opts.size.bboxSides ), y: cy - ( handle.isCorner ? ( ft.opts.customCorners ? Math.ceil(ft.opts.customCorners.size / 2) : ft.opts.size.bboxCorners ) : ft.opts.size.bboxSides ) }) .transform(ft.opts.customCorners ? null : 'R' + ft.attrs.rotate); handle.x = bboxHandleDirection[i][0]; handle.y = bboxHandleDirection[i][1]; }); } } if ( ft.circle ) { ft.circle.attr({ cx: ft.attrs.center.x + ft.attrs.translate.x, cy: ft.attrs.center.y + ft.attrs.translate.y, r: Math.max(radius.x, radius.y) * ft.opts.distance }); } if ( ft.handles.center ) { ft.handles.center.disc.toFront().attr({ cx: ft.attrs.center.x + ft.attrs.translate.x, cy: ft.attrs.center.y + ft.attrs.translate.y }); } if ( ft.opts.rotate.indexOf('self') >= 0 ) { radius = Math.max( Math.sqrt(Math.pow(corners[1].x - corners[0].x, 2) + Math.pow(corners[1].y - corners[0].y, 2)), Math.sqrt(Math.pow(corners[2].x - corners[1].x, 2) + Math.pow(corners[2].y - corners[1].y, 2)) ) / 2; } return ft; }; /** * Add handles */ ft.showHandles = function() { var i, handle, rotate, scale, handles = []; draggables = []; ft.hideHandles(); ft.axes.map(function(axis) { ft.handles[axis] = {}; ft.handles[axis].line = paper .path([ 'M', ft.attrs.center.x, ft.attrs.center.y ]) .attr({ stroke: ft.opts.attrs.stroke, 'stroke-dasharray': '- ', opacity: .5 }); ft.handles[axis].disc = paper .circle(ft.attrs.center.x, ft.attrs.center.y, ft.opts.size.axes) .attr(ft.opts.attrs); ft.handles[axis].disc.node.setAttribute('class', 'handle arm axis-' + axis); }); if ( ft.opts.draw.indexOf('bbox') >= 0 ) { ft.bbox = paper .path('') .attr({ stroke: ft.opts.attrs.stroke, 'stroke-dasharray': '- ', opacity: .5 }); ft.handles.bbox = []; for ( i = ( ft.opts.scale.indexOf('bboxCorners') || ft.opts.customCorners >= 0 ? 0 : 4 ); i < ( ft.opts.scale.indexOf('bboxSides') === -1 ? 4 : 8 ); i ++ ) { handle = {}; handle.index = i; handle.axis = i % 2 ? 'x' : 'y'; handle.isCorner = i < 4; if ( handle.isCorner && ft.opts.customCorners ) { handle.element = paper .image(ft.opts.customCorners.corners[i].image, ft.attrs.center.x, ft.attrs.center.y, ft.opts.customCorners.size, ft.opts.customCorners.size); } else { handle.element = paper .rect(ft.attrs.center.x, ft.attrs.center.y, ft.opts.size[handle.isCorner ? 'bboxCorners' : 'bboxSides' ] * 2, ft.opts.size[handle.isCorner ? 'bboxCorners' : 'bboxSides' ] * 2) .attr(ft.opts.attrs); if ( handle.isCorner ) { handle.element.node.setAttribute('class', 'handle bbox corner index-' + i + ' axis-' + handle.axis); } else { handle.element.node.setAttribute('class', 'handle bbox side index-' + ( i - 4 ) + ' axis-' + handle.axis); } } ft.handles.bbox[i] = handle; } } if ( ft.opts.draw.indexOf('circle') !== -1 ) { ft.circle = paper .circle(0, 0, 0) .attr({ stroke: ft.opts.attrs.stroke, 'stroke-dasharray': '- ', opacity: .3 }); } if ( ft.opts.drag.indexOf('center') !== -1 ) { ft.handles.center = {}; ft.handles.center.disc = paper .circle(ft.attrs.center.x, ft.attrs.center.y, ft.opts.size.center) .attr(ft.opts.attrs); ft.handles.center.disc.node.setAttribute('class', 'handle center'); } // Drag x, y handles and custom corners ft.axes.map(function(axis) { if ( ft.handles[axis] ) { handles.push({ element: ft.handles[axis].disc, rotate: ft.opts.rotate.indexOf('axis' + axis.toUpperCase()) !== -1, scale: ft.opts.scale .indexOf('axis' + axis.toUpperCase()) !== -1, axis: axis, isCorner: false }); } }); if ( ft.opts.customCorners ) { ft.handles.bbox.map(function(handle) { var action; if ( typeof ft.opts.customCorners.corners[handle.index] !== 'undefined' ) { action = ft.opts.customCorners.corners[handle.index].action; if ( action === 'rotate' || action === 'scale' ) { handles.push({ element: handle.element, rotate: action === 'rotate', scale: action === 'scale', axis: 'x', isCorner: true }); } } }); } handles.map(function(handle) { handle.element.drag(function(dx, dy) { var cx, cy, rad, opposite, adjacent, hypotenuse, pos, mirrored = { x: ft.o.scale.x < 0, y: ft.o.scale.y < 0 }; // viewBox might be scaled if ( ft.o.viewBoxRatio ) { dx *= ft.o.viewBoxRatio.x; dy *= ft.o.viewBoxRatio.y; } cx = dx + handle.element.ox, cy = dy + handle.element.oy; if ( handle.rotate ) { rad = Math.atan2(cy - ft.o.center.y - ft.o.translate.y, cx - ft.o.center.x - ft.o.translate.x); ft.attrs.rotate = rad * 180 / Math.PI - ft.o.angle; if ( mirrored[handle.axis] ) { ft.attrs.rotate -= 180; } } // Keep handle within boundaries if ( ft.opts.boundary ) { cx = Math.max(Math.min(cx, ft.opts.boundary.x + ( ft.opts.boundary.width || getPaperSize().x * (ft.o.viewBoxRatio && ft.o.viewBoxRatio.x || 1))), ft.opts.boundary.x); cy = Math.max(Math.min(cy, ft.opts.boundary.y + ( ft.opts.boundary.height || getPaperSize().y * (ft.o.viewBoxRatio && ft.o.viewBoxRatio.y || 1))), ft.opts.boundary.y); } opposite = cx - ft.o.center.x - ft.o.translate.x - ( ft.opts.customCorners ? ft.opts.customCorners.distance : 0 ), adjacent = cy - ft.o.center.y - ft.o.translate.y - ( ft.opts.customCorners ? ft.opts.customCorners.distance : 0 ), hypotenuse = Math.sqrt(Math.pow(opposite, 2) + Math.pow(adjacent, 2)); if ( handle.scale ) { ft.attrs.scale[handle.axis] = hypotenuse / ( ft.o.size[handle.axis] / 2 * ft.o.distance ); if ( mirrored[handle.axis] ) { ft.attrs.scale[handle.axis] *= -1; } } applyLimits(); // Maintain aspect ratio if ( ft.opts.keepRatio.indexOf('axis' + handle.axis.toUpperCase()) !== -1 ) { keepRatio(handle.axis); } else { ft.attrs.ratio = ft.attrs.scale.x / ft.attrs.scale.y; } if ( ft.attrs.scale.x && ft.attrs.scale.y ) { ft.apply(); } asyncCallback([ handle.rotate ? 'rotate' : null, handle.scale ? 'scale' : null ]); }, function() { // Offset values ft.o = cloneObj(ft.attrs); if ( paper._viewBox ) { ft.o.viewBoxRatio = { x: paper._viewBox[2] / getPaperSize().x, y: paper._viewBox[3] / getPaperSize().y }; } handle.element.ox = this.attrs.cx ? this.attrs.cx : this.attrs.x + this.attrs.width / 2; handle.element.oy = this.attrs.cy ? this.attrs.cy : this.attrs.y + this.attrs.height / 2; if ( handle.isCorner ) { hypotenuse = Math.sqrt(Math.pow(ft.o.size.x * ft.o.scale.x, 2) + Math.pow(ft.o.size.y * ft.o.scale.y, 2)) / 2, opposite = ft.o.size.x * ft.o.scale.x / 2, pos = { x: handle.element.ox - ( ft.o.center.x + ft.o.x + ft.o.translate.x ), y: handle.element.oy - ( ft.o.center.y + ft.o.y + ft.o.translate.y ) }; // Distance from centre of handle to centre of element ft.o.distance = hypotenuse / opposite; ft.o.angle = 90 - ( Math.atan2(pos.x, pos.y) * 180 / Math.PI ) - ft.o.rotate; } else { ft.o.distance = ft.opts.distance; ft.o.angle = handle.axis === 'y' ? 90 : 0; } asyncCallback([ handle.rotate ? 'rotate start' : null, handle.scale ? 'scale start' : null ]); }, function() { asyncCallback([ handle.rotate ? 'rotate end' : null, handle.scale ? 'scale end' : null ]); }); }); // Drag bbox handles if ( ft.opts.draw.indexOf('bbox') >= 0 && !ft.opts.customCorners && ( ft.opts.scale.indexOf('bboxCorners') !== -1 || ft.opts.scale.indexOf('bboxSides') !== -1 ) ) { ft.handles.bbox.map(function(handle) { handle.element.drag(function(dx, dy) { var sin, cos, rx, ry, rdx, rdy, mx, my, sx, sy, ratio, tdy, tdx, previous = cloneObj(ft.attrs), trans = { x: 0, y: 0 }; // viewBox might be scaled if ( ft.o.viewBoxRatio ) { dx *= ft.o.viewBoxRatio.x; dy *= ft.o.viewBoxRatio.y; } sin = ft.o.rotate.sin; cos = ft.o.rotate.cos; // First rotate dx, dy to element alignment rx = dx * cos - dy * sin; ry = dx * sin + dy * cos; rx *= Math.abs(handle.x); ry *= Math.abs(handle.y); // And finally rotate back to canvas alignment rdx = rx * cos + ry * sin; rdy = rx * - sin + ry * cos; ft.attrs.translate = { x: ft.o.translate.x + rdx / 2, y: ft.o.translate.y + rdy / 2 }; // Mouse position, relative to element center after translation mx = ft.o.handlePos.cx + dx - ft.attrs.center.x - ft.attrs.translate.x; my = ft.o.handlePos.cy + dy - ft.attrs.center.y - ft.attrs.translate.y; // Position rotated to align with element rx = mx * cos - my * sin; ry = mx * sin + my * cos; // Maintain aspect ratio if ( handle.isCorner && ft.opts.keepRatio.indexOf('bboxCorners') !== -1 ) { ratio = ( ft.attrs.size.x * ft.attrs.scale.x ) / ( ft.attrs.size.y * ft.attrs.scale.y ), tdy = rx * handle.x * ( 1 / ratio ), tdx = ry * handle.y * ratio; if ( tdx > tdy * ratio ) { rx = tdx * handle.x; } else { ry = tdy * handle.y; } } // Scale element so that handle is at mouse position sx = rx * 2 * handle.x / ft.o.size.x; sy = ry * 2 * handle.y / ft.o.size.y; ft.attrs.scale = { x: sx || ft.attrs.scale.x, y: sy || ft.attrs.scale.y }; // Check boundaries if ( !isWithinBoundaries().x || !isWithinBoundaries().y ) { ft.attrs = previous; } applyLimits(); // Maintain aspect ratio if ( ( handle.isCorner && ft.opts.keepRatio.indexOf('bboxCorners') !== -1 ) || ( !handle.isCorner && ft.opts.keepRatio.indexOf('bboxSides') !== -1 ) ) { keepRatio(handle.axis); trans.x = ( ft.attrs.scale.x - ft.o.scale.x ) * ft.o.size.x * handle.x; trans.y = ( ft.attrs.scale.y - ft.o.scale.y ) * ft.o.size.y * handle.y; rx = trans.x * cos + trans.y * sin; ry = - trans.x * sin + trans.y * cos; ft.attrs.translate.x = ft.o.translate.x + rx / 2; ft.attrs.translate.y = ft.o.translate.y + ry / 2; } ft.attrs.ratio = ft.attrs.scale.x / ft.attrs.scale.y; asyncCallback([ 'scale' ]); ft.apply(); }, function() { var angle = ( ( 360 - ft.attrs.rotate ) % 360 ) / 180 * Math.PI, handlePos = handle.element.attr(['x', 'y']); // Offset values ft.o = cloneObj(ft.attrs); ft.o.handlePos = { cx: handlePos.x + ft.opts.size[handle.isCorner ? 'bboxCorners' : 'bboxSides'], cy: handlePos.y + ft.opts.size[handle.isCorner ? 'bboxCorners' : 'bboxSides'] }; // Pre-compute rotation sin & cos for efficiency ft.o.rotate = { sin: Math.sin(angle), cos: Math.cos(angle) }; if ( paper._viewBox ) { ft.o.viewBoxRatio = { x: paper._viewBox[2] / getPaperSize().x, y: paper._viewBox[3] / getPaperSize().y }; } asyncCallback([ 'scale start' ]); }, function() { asyncCallback([ 'scale end' ]); }); }); } // Custom BBox corners if ( ft.opts.customCorners ) { ft.handles.bbox.map(function(handle) { if ( typeof ft.opts.customCorners.corners[handle.index] !== 'undefined' ) { if ( [ 'drag', 'rotate', 'scale' ].indexOf(ft.opts.customCorners.corners[handle.index].action) === -1 ) { // Unknown action, absorb drag event and emit as custom event on click handle.element.drag(function() { return false; }); handle.element.click(function() { asyncCallback([ 'custom:' + ft.opts.customCorners.corners[handle.index].action ]); }); } } }); } // Drag element and center handle if ( ft.opts.drag.indexOf('self') >= 0 && ft.opts.scale.indexOf('self') === -1 && ft.opts.rotate.indexOf('self') === -1 ) { draggables.push(subject); } if ( ft.opts.drag.indexOf('center') >= 0 ) { draggables.push(ft.handles.center.disc); } if ( ft.opts.customCorners ) { ft.handles.bbox.map(function(handle) { if ( typeof ft.opts.customCorners.corners[handle.index] !== 'undefined' ) { if ( ft.opts.customCorners.corners[handle.index].action === 'drag' ) { draggables.push(handle.element); } } }); } draggables.map(function(draggable) { draggable.drag(function(dx, dy) { var bbox = cloneObj(ft.o.bbox); // viewBox might be scaled if ( ft.o.viewBoxRatio ) { dx *= ft.o.viewBoxRatio.x; dy *= ft.o.viewBoxRatio.y; } ft.attrs.translate.x = ft.o.translate.x + dx; ft.attrs.translate.y = ft.o.translate.y + dy; bbox.x += dx; bbox.y += dy; applyLimits(bbox); asyncCallback([ 'drag' ]); ft.apply(); }, function() { // Offset values ft.o = cloneObj(ft.attrs); if ( ft.opts.snap.drag ) { ft.o.bbox = subject.getBBox(); } // viewBox might be scaled if ( paper._viewBox ) { ft.o.viewBoxRatio = { x: paper._viewBox[2] / getPaperSize().x, y: paper._viewBox[3] / getPaperSize().y }; } ft.axes.map(function(axis) { if ( ft.handles[axis] ) { ft.handles[axis].disc.ox = ft.handles[axis].disc.attrs.cx; ft.handles[axis].disc.oy = ft.handles[axis].disc.attrs.cy; } }); asyncCallback([ 'drag start' ]); }, function() { asyncCallback([ 'drag end' ]); }); }); rotate = ft.opts.rotate.indexOf('self') >= 0, scale = ft.opts.scale .indexOf('self') >= 0; if ( rotate || scale ) { subject.drag(function(dx, dy, x, y) { var rad, radius, mirrored = { x: ft.o.scale.x < 0, y: ft.o.scale.y < 0 }; if ( rotate ) { rad = Math.atan2(y - ft.o.center.y - ft.o.translate.y, x - ft.o.center.x - ft.o.translate.x); ft.attrs.rotate = ft.o.rotate + ( rad * 180 / Math.PI ) - ft.o.deg; } if ( scale ) { radius = Math.sqrt(Math.pow(x - ft.o.center.x - ft.o.translate.x, 2) + Math.pow(y - ft.o.center.y - ft.o.translate.y, 2)); ft.attrs.scale.x = ft.attrs.scale.y = ( mirrored.x ? -1 : 1 ) * ft.o.scale.x + ( radius - ft.o.radius ) / ( ft.o.size.x / 2 ); if ( mirrored.x ) { ft.attrs.scale.x *= -1; } if ( mirrored.y ) { ft.attrs.scale.y *= -1; } } applyLimits(); ft.apply(); asyncCallback([ rotate ? 'rotate' : null, scale ? 'scale' : null ]); }, function(x, y) { // Offset values ft.o = cloneObj(ft.attrs); ft.o.deg = Math.atan2(y - ft.o.center.y - ft.o.translate.y, x - ft.o.center.x - ft.o.translate.x) * 180 / Math.PI; ft.o.radius = Math.sqrt(Math.pow(x - ft.o.center.x - ft.o.translate.x, 2) + Math.pow(y - ft.o.center.y - ft.o.translate.y, 2)); // viewBox might be scaled if ( paper._viewBox ) { ft.o.viewBoxRatio = { x: paper._viewBox[2] / getPaperSize().x, y: paper._viewBox[3] / getPaperSize().y }; } asyncCallback([ rotate ? 'rotate start' : null, scale ? 'scale start' : null ]); }, function() { asyncCallback([ rotate ? 'rotate end' : null, scale ? 'scale end' : null ]); }); } ft.updateHandles(); return ft; }; /** * Remove handles */ ft.hideHandles = function(opts) { var opts = opts || {} if ( opts.undrag === undefined ) { opts.undrag = true; } if ( opts.undrag ) { ft.items.map(function(item) { item.el.undrag(); }); } if ( ft.handles.center ) { ft.handles.center.disc.remove(); ft.handles.center = null; } [ 'x', 'y' ].map(function(axis) { if ( ft.handles[axis] ) { ft.handles[axis].disc.remove(); ft.handles[axis].line.remove(); ft.handles[axis] = null; } }); if ( ft.bbox ) { ft.bbox.remove(); ft.bbox = null; if ( ft.handles.bbox ) { ft.handles.bbox.map(function(handle) { handle.element.remove(); }); ft.handles.bbox = null; } } if ( ft.circle ) { ft.circle.remove(); ft.circle = null; } return ft; }; // Override defaults ft.setOpts = function(options, callback) { var i, j; if ( callback !== undefined ) { ft.callback = typeof callback === 'function' ? callback : false; } for ( i in options ) { if ( options[i] && options[i].constructor === Object ) { if(ft.opts[i] === false){ ft.opts[i] = {}; } for ( j in options[i] ) { if ( options[i].hasOwnProperty(j) ) { ft.opts[i][j] = options[i][j]; } } } else { ft.opts[i] = options[i]; } } if ( ft.opts.animate === true ) { ft.opts.animate = { delay: 700, easing: 'linear' }; } if ( ft.opts.drag === true ) { ft.opts.drag = [ 'center', 'self' ]; } if ( ft.opts.keepRatio === true ) { ft.opts.keepRatio = [ 'axisX', 'axisY', 'bboxCorners', 'bboxSides' ]; } if ( ft.opts.rotate === true ) { ft.opts.rotate = [ 'axisX', 'axisY' ]; } if ( ft.opts.scale === true ) { ft.opts.scale = [ 'axisX', 'axisY', 'bboxCorners', 'bboxSides' ]; } [ 'drag', 'draw', 'keepRatio', 'rotate', 'scale' ].map(function(option) { if ( ft.opts[option] === false ) { ft.opts[option] = []; } }); ft.axes = []; if ( ft.opts.rotate.indexOf('axisX') >= 0 || ft.opts.scale.indexOf('axisX') >= 0 ) { ft.axes.push('x'); } if ( ft.opts.rotate.indexOf('axisY') >= 0 || ft.opts.scale.indexOf('axisY') >= 0 ) { ft.axes.push('y'); } [ 'drag', 'rotate', 'scale' ].map(function(option) { if ( !ft.opts.snapDist[option] ) { ft.opts.snapDist[option] = ft.opts.snap[option]; } }); // Force numbers ft.opts.range = { rotate: [ parseFloat(ft.opts.range.rotate[0]), parseFloat(ft.opts.range.rotate[1]) ], scale: [ parseFloat(ft.opts.range.scale[0]), parseFloat(ft.opts.range.scale[1]) ] }; ft.opts.snap = { drag: parseFloat(ft.opts.snap.drag) || 0, rotate: parseFloat(ft.opts.snap.rotate) || 0, scale: parseFloat(ft.opts.snap.scale) || 0 }; ft.opts.snapDist = { drag: parseFloat(ft.opts.snapDist.drag) || 0, rotate: parseFloat(ft.opts.snapDist.rotate) || 0, scale: parseFloat(ft.opts.snapDist.scale) || 0 }; if ( typeof ft.opts.size === 'string' ) { ft.opts.size = parseFloat(ft.opts.size) || 0; } if ( !isNaN(ft.opts.size) ) { ft.opts.size = { axes: ft.opts.size, bboxCorners: ft.opts.size, bboxSides: ft.opts.size, center: ft.opts.size }; } if ( ft.opts.customCorners ) { ft.opts.customCorners.size = parseFloat(ft.opts.customCorners.size) || 0; ft.opts.customCorners.distance = parseFloat(ft.opts.customCorners.distance) || 0; } ft.showHandles(); asyncCallback([ 'init' ]); return ft; }; ft.setOpts(options, callback); /** * Apply transformations, optionally update attributes manually */ ft.apply = function() { ft.items.map(function(item, i) { // Take offset values into account var center = { x: ft.attrs.center.x + ft.offset.translate.x, y: ft.attrs.center.y + ft.offset.translate.y }, rotate = ft.attrs.rotate - ft.offset.rotate, scale = { x: ft.attrs.scale.x / ft.offset.scale.x, y: ft.attrs.scale.y / ft.offset.scale.y }, translate = { x: ft.attrs.translate.x - ft.offset.translate.x, y: ft.attrs.translate.y - ft.offset.translate.y }; if ( ft.opts.animate ) { asyncCallback([ 'animate start' ]); item.el.animate( { transform: [ 'R', rotate, center.x, center.y, 'S', scale.x, scale.y, center.x, center.y, 'T', translate.x, translate.y ] + ft.items[i].transformString }, ft.opts.animate.delay, ft.opts.animate.easing, function() { asyncCallback([ 'animate end' ]); ft.updateHandles(); } ); } else { item.el.transform([ 'R', rotate, center.x, center.y, 'S', scale.x, scale.y, center.x, center.y, 'T', translate.x, translate.y ] + ft.items[i].transformString); asyncCallback([ 'apply' ]); ft.updateHandles(); } }); return ft; }; /** * Clean exit */ ft.unplug = function() { var attrs = ft.attrs; ft.hideHandles(); // Goodbye delete subject.freeTransform; return attrs; }; // Store attributes for each item function scan(subject) { ( subject.type === 'set' ? subject.items : [ subject ] ).map(function(item) { if ( item.type === 'set' ) { scan(item); } else { ft.items.push({ el: item, attrs: { rotate: 0, scale: { x: 1, y: 1 }, translate: { x: 0, y: 0 } }, transformString: item.matrix.toTransformString() }); } }); } scan(subject); // Get the current transform values for each item ft.items.map(function(item, i) { if ( item.el._ && item.el._.transform && typeof item.el._.transform === 'object' ) { item.el._.transform.map(function(transform) { if ( transform[0] ) { switch ( transform[0].toUpperCase() ) { case 'T': ft.items[i].attrs.translate.x += transform[1]; ft.items[i].attrs.translate.y += transform[2]; break; case 'S': ft.items[i].attrs.scale.x *= transform[1]; ft.items[i].attrs.scale.y *= transform[2]; break; case 'R': ft.items[i].attrs.rotate += transform[1]; break; } } }); } }); // If subject is not of type set, the first item _is_ the subject if ( subject.type !== 'set' ) { ft.attrs.rotate = ft.items[0].attrs.rotate; ft.attrs.scale = ft.items[0].attrs.scale; ft.attrs.translate = ft.items[0].attrs.translate; ft.items[0].attrs = { rotate: 0, scale: { x: 1, y: 1 }, translate: { x: 0, y: 0 } }; ft.items[0].transformString = ''; } ft.attrs.ratio = ft.attrs.scale.x / ft.attrs.scale.y; /** * Get rotated bounding box */ function getBBox(distance) { var rad, radius, corners = [], signs = [ { x: -1, y: -1 }, { x: 1, y: -1 }, { x: 1, y: 1 }, { x: -1, y: 1 } ]; if ( typeof distance !== 'number' ) { distance = 0; } rad = { x: ( ft.attrs.rotate ) * Math.PI / 180, y: ( ft.attrs.rotate + 90 ) * Math.PI / 180 }; radius = { x: ft.attrs.size.x / 2 * ft.attrs.scale.x + distance, y: ft.attrs.size.y / 2 * ft.attrs.scale.y + distance }; signs.map(function(sign) { corners.push({ x: ( ft.attrs.center.x + ft.attrs.translate.x + sign.x * radius.x * Math.cos(rad.x) ) + sign.y * radius.y * Math.cos(rad.y), y: ( ft.attrs.center.y + ft.attrs.translate.y + sign.x * radius.x * Math.sin(rad.x) ) + sign.y * radius.y * Math.sin(rad.y) }); }); return corners; } /** * Get dimension of the paper */ function getPaperSize() { var match = { x: /^([0-9]+)%$/.exec(paper.width), y: /^([0-9]+)%$/.exec(paper.height) }; return { x: match.x ? paper.canvas.clientWidth || paper.canvas.parentNode.clientWidth * parseInt(match.x[1], 10) * 0.01 : paper.canvas.clientWidth || paper.width, y: match.y ? paper.canvas.clientHeight || paper.canvas.parentNode.clientHeight * parseInt(match.y[1], 10) * 0.01 : paper.canvas.clientHeight || paper.height }; } /** * Apply limits */ function applyLimits(bbox) { var x, y, deg, dist = { x: 0, y: 0 }, snap = { x: 0, y: 0 }; // Snap to grid if ( bbox && ft.opts.snap.drag ) { x = bbox.x, y = bbox.y, [ 0, 1 ].map(function() { // Top and left sides first dist.x = x - Math.round(x / ft.opts.snap.drag) * ft.opts.snap.drag; dist.y = y - Math.round(y / ft.opts.snap.drag) * ft.opts.snap.drag; if ( Math.abs(dist.x) <= ft.opts.snapDist.drag ) { snap.x = dist.x; } if ( Math.abs(dist.y) <= ft.opts.snapDist.drag ) { snap.y = dist.y; } // Repeat for bottom and right sides x += bbox.width - snap.x; y += bbox.height - snap.y; }); ft.attrs.translate.x -= snap.x; ft.attrs.translate.y -= snap.y; } // Keep center within boundaries if ( ft.opts.boundary ) { ft.opts.boundarywidth = ft.opts.boundarywidth || getPaperSize().x * (ft.o.viewBoxRatio && ft.o.viewBoxRatio.x || 1); ft.opts.boundaryheight = ft.opts.boundaryheight || getPaperSize().y * (ft.o.viewBoxRatio && ft.o.viewBoxRatio.y || 1); if ( ft.attrs.center.x + ft.attrs.translate.x < ft.opts.boundary.x ) { ft.attrs.translate.x += ft.opts.boundary.x - ( ft.attrs.center.x + ft.attrs.translate.x ); } if ( ft.attrs.center.y + ft.attrs.translate.y < ft.opts.boundary ) { ft.attrs.translate.y += ft.opts.boundary - ( ft.attrs.center.y + ft.attrs.translate.y ); } if ( ft.attrs.center.x + ft.attrs.translate.x > ft.opts.boundary.x + ft.opts.boundarywidth ) { ft.attrs.translate.x += ft.opts.boundary.x + ft.opts.boundarywidth - ( ft.attrs.center.x + ft.attrs.translate.x ); } if ( ft.attrs.center.y + ft.attrs.translate.y > ft.opts.boundary + ft.opts.boundaryheight ) { ft.attrs.translate.y += ft.opts.boundary + ft.opts.boundaryheight - ( ft.attrs.center.y + ft.attrs.translate.y ); } } // Snap to angle, rotate with increments dist = Math.abs(ft.attrs.rotate % ft.opts.snap.rotate); dist = Math.min(dist, ft.opts.snap.rotate - dist); if ( dist < ft.opts.snapDist.rotate ) { ft.attrs.rotate = Math.round(ft.attrs.rotate / ft.opts.snap.rotate) * ft.opts.snap.rotate; } // Snap to scale, scale with increments dist = { x: Math.abs(( ft.attrs.scale.x * ft.attrs.size.x ) % ft.opts.snap.scale), y: Math.abs(( ft.attrs.scale.y * ft.attrs.size.x ) % ft.opts.snap.scale) }; dist = { x: Math.min(dist.x, ft.opts.snap.scale - dist.x), y: Math.min(dist.y, ft.opts.snap.scale - dist.y) }; if ( dist.x < ft.opts.snapDist.scale ) { ft.attrs.scale.x = Math.round(ft.attrs.scale.x * ft.attrs.size.x / ft.opts.snap.scale) * ft.opts.snap.scale / ft.attrs.size.x; } if ( dist.y < ft.opts.snapDist.scale ) { ft.attrs.scale.y = Math.round(ft.attrs.scale.y * ft.attrs.size.y / ft.opts.snap.scale) * ft.opts.snap.scale / ft.attrs.size.y; } // Limit range of rotation if ( ft.opts.range.rotate ) { deg = ( 360 + ft.attrs.rotate ) % 360; if ( deg > 180 ) { deg -= 360; } if ( deg < ft.opts.range.rotate[0] ) { ft.attrs.rotate += ft.opts.range.rotate[0] - deg; } if ( deg > ft.opts.range.rotate[1] ) { ft.attrs.rotate += ft.opts.range.rotate[1] - deg; } } // Limit scale if ( ft.opts.range.scale ) { if ( ft.attrs.scale.x * ft.attrs.size.x < ft.opts.range.scale[0] ) { ft.attrs.scale.x = ft.opts.range.scale[0] / ft.attrs.size.x; } if ( ft.attrs.scale.y * ft.attrs.size.y < ft.opts.range.scale[0] ) { ft.attrs.scale.y = ft.opts.range.scale[0] / ft.attrs.size.y; } if ( ft.attrs.scale.x * ft.attrs.size.x > ft.opts.range.scale[1] ) { ft.attrs.scale.x = ft.opts.range.scale[1] / ft.attrs.size.x; } if ( ft.attrs.scale.y * ft.attrs.size.y > ft.opts.range.scale[1] ) { ft.attrs.scale.y = ft.opts.range.scale[1] / ft.attrs.size.y; } } } function isWithinBoundaries() { return { x: ft.attrs.scale.x * ft.attrs.size.x >= ft.opts.range.scale[0] && ft.attrs.scale.x * ft.attrs.size.x <= ft.opts.range.scale[1], y: ft.attrs.scale.y * ft.attrs.size.y >= ft.opts.range.scale[0] && ft.attrs.scale.y * ft.attrs.size.y <= ft.opts.range.scale[1] }; } function keepRatio(axis) { if ( axis === 'x' ) { ft.attrs.scale.y = ft.attrs.scale.x / ft.attrs.ratio; } else { ft.attrs.scale.x = ft.attrs.scale.y * ft.attrs.ratio; } } /** * Recursive copy of object */ function cloneObj(obj) { var i, clone = {}; for ( i in obj ) { clone[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i]; } return clone; } /** * Call callback asynchronously for better performance */ function asyncCallback(e) { var events = []; if ( ft.callback ) { // Remove empty values e.map(function(e, i) { if ( e ) { events.push(e); } }); clearTimeout(timeout); timeout = setTimeout(function() { if ( ft.callback ) { ft.callback(ft, events, getBBox()); } }, .1); } } ft.updateHandles(); // Enable method chaining return ft; }; }));