(function(){
angular.module('svgApp', []);
// creating the main module for the app
})()
<!DOCTYPE html>
<html ng-app="svgApp">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>
document.write('<base href="' + document.location + '" />');
</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js@1.5.x" src="https://code.angularjs.org/1.5.8/angular.js" data-semver="1.5.8">
</script>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg svg-directive="" ng-controller="SvgCtrl as vm" width="100%" height="100%" viewBox="0 0 1000 1000" style="border:1px solid black"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="elem-group">
<image xlink:href="" class="image-element" preserveAspectRatio="none" ng-attr-transform="{{vm.model.image.transform.translate}}{{vm.model.image.transform.rotate}}"
ng-attr-xlink:href="{{vm.model.image.url}}" ng-attr-width="{{vm.model.image.width}}" ng-attr-height="{{vm.model.image.height}}"
ng-attr-x="{{vm.model.image.x}}" ng-attr-y="{{vm.model.image.y}}"></image>
</g>
<g class="controls-group" ng-attr-transform="{{vm.model.image.transform.translate}}{{vm.model.image.transform.rotate}}">
<!-- the main rect that is over the element -->
<rect class="move-rect" fill-opacity="0" stroke="#64686f" stroke-width="2" ng-attr-x="{{vm.model.image.x}}" ng-attr-y="{{vm.model.image.y}}"
ng-attr-width="{{vm.model.image.width}}" ng-attr-height="{{vm.model.image.height}}"></rect>
<!-- the resize handles -->
<rect class="resize-tl" ng-attr-x="{{vm.model.image.x-10}}" ng-attr-y="{{vm.model.image.y-10}}" fill="white" stroke="#64686f"
stroke-width="2" width="20" height="20"></rect>
<rect class="resize-tr" ng-attr-x="{{vm.model.image.x+vm.model.image.width-10}}" ng-attr-y="{{vm.model.image.y-10}}" fill="red"
stroke="#64686f" stroke-width="2" width="20" height="20"></rect>
<rect class="resize-bl" ng-attr-x="{{vm.model.image.x-10}}" ng-attr-y="{{vm.model.image.y+vm.model.image.height-10}}" fill="blue"
stroke="#64686f" stroke-width="2" width="20" height="20"></rect>
<rect class="resize-br" ng-attr-x="{{vm.model.image.x+vm.model.image.width-10}}" ng-attr-y="{{vm.model.image.y+vm.model.image.height-10}}"
fill="yellow" stroke="#64686f" stroke-width="2" width="20" height="20"></rect>
<!-- the rotate handles -->
<circle class="rotate-tl" fill="white" stroke="#64686f" stroke-width="2" ng-attr-cx="{{vm.model.image.x-20}}" ng-attr-cy="{{vm.model.image.y-20}}"
r="12"></circle>
<circle class="rotate-tr" fill="red" stroke="#64686f" stroke-width="2" ng-attr-cx="{{(vm.model.image.width+vm.model.image.x)+20}}"
ng-attr-cy="{{vm.model.image.y-20}}" r="12"></circle>
<circle class="rotate-bl" fill="blue" stroke="#64686f" stroke-width="2" ng-attr-cx="{{vm.model.image.x-20}}" ng-attr-cy="{{(vm.model.image.height+vm.model.image.y)+20}}"
r="12"></circle>
<circle class="rotate-br" fill="yellow" stroke="#64686f" stroke-width="2" ng-attr-cx="{{(vm.model.image.width+vm.model.image.x)+20}}"
ng-attr-cy="{{(vm.model.image.height+vm.model.image.y)+20}}" r="12"></circle>
</g>
<!-- this circle is not in the center of the element after the angle !== 0 -->
<g class="g-red-center-circle">
<circle class="red-center-circle" fill="red" stroke-width="1" stroke="white" r="5" ng-attr-cx="{{vm.model.image.rotate.cx}}"
ng-attr-cy="{{vm.model.image.rotate.cy}}" ng-attr-transform="{{vm.model.image.transform.translate}}"></circle>
</g>
<!-- this circle is in the correct center, even after the angle !== -->
<g class="g-blue-center-circle">
<circle class="blue-center-circle" fill="blue" stroke-width="1" stroke="white" r="5" ng-attr-cx="{{vm.model.image.rotate.cx}}"
ng-attr-cy="{{vm.model.image.rotate.cy}}" ng-attr-transform="{{vm.model.image.transform.translate}}{{vm.model.image.transform.rotate}}"></circle>
</g>
</svg>
<script src="svgApp.module.js"></script>
<script src="svgApp.config.js"></script>
<script src="svgApp.service.js"></script>
<script src="svgApp.controller.js"></script>
<script src="svgApp.directive.js"></script>
</body>
</html>
/* Put your css in here */
(function(){
function SvgService(){
/**
* Service model object
* */
var model = {
image : {
url : "http://i.imgur.com/hSqdOZI.jpg",
width : 680,
height : 510,
x : 50, y : 50,
rotate : {
angle : 0, cx : 0, cy : 0
},
translate : {
x : 0, y : 0
},
transform : {
rotate : null, translate : null
}
}
};
// exports public functions and variables
return {
// variables
model : model,
// functions
updateImageTransform : updateImageTransform,
updateImageCoordinates : updateImageCoordinates,
getImageUpdatedCoordinates : getImageUpdatedCoordinates,
updateImageRotateCoordinates : updateImageRotateCoordinates,
updateImageTranslateCoordinates : updateImageTranslateCoordinates,
getImageUpdatedRotateCoordinates : getImageUpdatedRotateCoordinates,
getImageUpdatedTranslateCoordinates : getImageUpdatedTranslateCoordinates
};
/////////////////////////////////////////////////////////////
function updateImageTranslateCoordinates(x, y){
model.image.translate.x = x || model.image.translate.x;
model.image.translate.y = y || model.image.translate.y;
}
function getImageUpdatedTranslateCoordinates(){
return model.image.translate;
}
/**
* Function to get the current image rotate coordinate values
* */
function getImageUpdatedRotateCoordinates(){
return model.image.rotate;
}
/**
* Function to get the current image coordinate values
*/
function getImageUpdatedCoordinates(){
return {
width : model.image.width,
height : model.image.height,
x : model.image.x,
y : model.image.y
};
}
/**
* Function to update the rotate values
* */
function updateImageRotateCoordinates(angle, cx, cy){
angle = angle || model.image.rotate.angle;
cx = cx || model.image.rotate.cx;
cy = cy || model.image.rotate.cy;
model.image.rotate.angle = angle;
model.image.rotate.cx = cx;
model.image.rotate.cy = cy;
}
/**
* Function to update the transform string value
* for the passed transform attr
* */
function updateImageTransform(transform, stringValue){
model.image.transform[transform] = stringValue;
}
/**
* Function to update the image position and dimension
* coordinate values
* */
function updateImageCoordinates(width, height, x, y){
width = width || model.image.width;
height = height || model.image.height;
x = x || model.image.x;
y = y || model.image.y;
model.image.width = width;
model.image.height = height;
model.image.x = x;
model.image.y = y;
}
}
angular
.module('svgApp')
.factory('SvgService', SvgService);
})()
(function() {
SvgCtrl.$inject = ['SvgService'];
function SvgCtrl(SvgService) {
var model = SvgService.model;
angular.extend(this, {
model: model,
});
}
angular.module('svgApp').controller('SvgCtrl', SvgCtrl);
})();
(function() {
SvgAppConfig.$inject = ['$sceDelegateProvider'];
function SvgAppConfig($sceDelegateProvider) {
// needed to load external images in svg
$sceDelegateProvider.resourceUrlWhitelist(['**']);
}
angular.module('svgApp').config(SvgAppConfig);
})();
(function() {
SvgDirective.$inject = ['SvgService'];
function SvgDirective(SvgService) {
function SvgDirectiveLink(scope, elem, attrs) {
// selecting svg elements via d3js
var mainSvg = d3.select(elem);
var controlsGroup = d3.select('g.controls-group');
var topLeftRotateHandleElem = controlsGroup.select('circle.rotate-tl');
var blueCenterCircle = d3.select('circle.blue-center-circle');
// when the directive is binded, stores the original
// coordinates of the image to be used as validation
// by the resize function
var originalCoordinates = SvgService.getImageUpdatedCoordinates();
// sets the minimun values for width and height to be of 10% of the original
// dimensions, to avoid negative values
var minWidth = 0.1 * originalCoordinates.width;
var minHeight = 0.1 * originalCoordinates.height;
// transform position, used to move the object around with the drag and drop
var tPos = SvgService.getImageUpdatedTranslateCoordinates();
var elemCenter = getElementCenter();
SvgService.updateImageRotateCoordinates(null, elemCenter.x, elemCenter.y);
// do the bind of the drag behavior in the controls elements
controlsGroup.call(bindControlsDragAndDrop());
/**
* Function used to bind the drag and drop behavior of the controls
*/
function bindControlsDragAndDrop() {
// auxiliar variables
var target, targetClass, rotateHandleStartPos;
// binding the behavior callback functions
var drag = d3.behavior
.drag()
.on('dragstart', dragStart)
.on('drag', dragMove)
.on('dragend', dragEnd);
return drag;
/**
* For the drag action starts
* */
function dragStart() {
// gets the current target element where the drag event started
target = d3.select(d3.event.sourceEvent.target);
// and saves the target element class in a aux variable
targetClass = target.attr('class');
// when the user is rotating the element, stores the initial angle
// information to be used in the rotate function
if (targetClass.indexOf('rotate') > -1) {
// gets the updated rotate coordinates
var updatedRotateCoordinates = SvgService.getImageUpdatedRotateCoordinates();
// updates the rotate handle start posistion object with
// basic information from the model and the handles
rotateHandleStartPos = {
angle: updatedRotateCoordinates.angle, // the current angle
x: parseFloat(target.attr('cx')), // the current cx value of the target handle
y: parseFloat(target.attr('cy')), // the current cy value of the target handle
};
// calc the rotated top & left corner
if (rotateHandleStartPos.angle > 0) {
var correctsRotateHandleStartPos = getHandleRotatePosition(
rotateHandleStartPos
);
rotateHandleStartPos.x = correctsRotateHandleStartPos.x;
rotateHandleStartPos.y = correctsRotateHandleStartPos.y;
}
// adds the initial angle in degrees
rotateHandleStartPos.iniAngle = calcAngleDeg(
updatedRotateCoordinates,
rotateHandleStartPos
);
}
}
/**
* For while the drag is happening
* */
function dragMove() {
// checks the target class to choose the right function
// to be executed while dragging
// #1 - If the user is moving the element around
if (targetClass.indexOf('move') > -1) {
moveObject();
}
// #2 - If the user is resizing the element
else if (targetClass.indexOf('resize') > -1) {
resizeObject(targetClass);
}
// #3 - If the user is rotating the element
else if (targetClass.indexOf('rotate') > -1) {
rotateObject(rotateHandleStartPos);
}
// apply the scope changes for any function that might
// have been called, to keep things updated in the service model
scope.$apply();
}
/**
* For when the drag stops (the user release the element)
* */
function dragEnd() {
// check if the user was resizing
if (targetClass.indexOf('resize') > -1) {
// updates the center of rotation after resizing the element
elemCenter = getElementCenter();
SvgService.updateImageRotateCoordinates(
null,
elemCenter.x,
elemCenter.y
);
scope.$apply();
}
}
}
/**
* Function to move the object around
* */
function moveObject() {
// increments the x/y values with the dx/dy values from the d3.event object
tPos.x += d3.event.dx;
tPos.y += d3.event.dy;
// updates the translate transform values to move the object to the new point
SvgService.updateImageTransform(
'translate',
'translate(' + [tPos.x, tPos.y] + ')'
);
// updates the translate coordinates in the model for further use
SvgService.updateImageTranslateCoordinates(tPos.x, tPos.y);
}
/**
* Function to resize the object based in the passed direction
* */
function resizeObject(direction) {
// gets the original image coordinates from the service model
var updatedCoordinates = SvgService.getImageUpdatedCoordinates();
// auxiliar variables
var x, y, width, height;
// do the resize math based in the direction that the resize started
switch (direction) {
// top left
case 'resize-tl':
width = updatedCoordinates.width - d3.event.dx;
height = updatedCoordinates.height - d3.event.dy;
x = updatedCoordinates.x + (updatedCoordinates.width - width);
y = updatedCoordinates.y + (updatedCoordinates.height - height);
break;
// top right
case 'resize-tr':
width = updatedCoordinates.width + d3.event.dx;
height = updatedCoordinates.height - d3.event.dy;
y = updatedCoordinates.y + (updatedCoordinates.height - height);
break;
// bottom left
case 'resize-bl':
width = updatedCoordinates.width - d3.event.dx;
height = updatedCoordinates.height + d3.event.dy;
x = updatedCoordinates.x + (updatedCoordinates.width - width);
break;
// bottom right
case 'resize-br':
width = updatedCoordinates.width + d3.event.dx;
height = updatedCoordinates.height + d3.event.dy;
break;
}
if (width > minWidth && height > minHeight) {
// updates the image object model with the new coordinates values
SvgService.updateImageCoordinates(width, height, x, y);
}
}
/**
* Function to rotate the object based in the initial rotation values
* present in the rotateHandleStartPos object
* */
function rotateObject(rotateHandleStartPos) {
// gets the current udapted rotate coordinates
var updatedRotateCoordinates = SvgService.getImageUpdatedRotateCoordinates();
// increments the mouse event starting point with the mouse movement event
rotateHandleStartPos.x += d3.event.dx;
rotateHandleStartPos.y += d3.event.dy;
// calculates the difference between the current mouse position and the center line
var angleFinal = calcAngleDeg(
updatedRotateCoordinates,
rotateHandleStartPos
);
// gets the difference of the angles to get to the final angle
var angle =
rotateHandleStartPos.angle +
angleFinal -
rotateHandleStartPos.iniAngle;
// converts the values to stay inside the 360 positive
angle %= 360;
if (angle < 0) {
angle += 360;
}
// creates the new rotate position array
var rotatePos = [
angle,
updatedRotateCoordinates.cx,
updatedRotateCoordinates.cy,
];
// updates the transform rotate string value and the rotate info in the service model
SvgService.updateImageTransform('rotate', 'rotate(' + rotatePos + ')');
// and updates the current angle with the new one
SvgService.updateImageRotateCoordinates(angle);
}
//////////////
/**
* Private functions
* */
// gets the passed d3 element center coordinates
function getElementCenter() {
var uCords = SvgService.getImageUpdatedCoordinates();
var result = {
x: uCords.x + uCords.width / 2,
y: uCords.y + uCords.height / 2,
};
return result;
}
/**
* Function to corrects the rotate handles starting position
*/
function getHandleRotatePosition(handleStartPos) {
// its possible to use "cx/cy" for properties
var originalX = handleStartPos.x ? handleStartPos.x : handleStartPos.cx;
var originalY = handleStartPos.y ? handleStartPos.y : handleStartPos.cy;
// gets the updated element center, without rotatio
var center = getElementCenter();
// calculates the rotated handle position considering the current center as
// pivot for rotation
var dx = originalX - center.x;
var dy = originalY - center.y;
var theta = (handleStartPos.angle * Math.PI) / 180;
return {
x: dx * Math.cos(theta) - dy * Math.sin(theta) + center.x,
y: dx * Math.sin(theta) + dy * Math.cos(theta) + center.y,
};
}
// gets the angle in degrees between two points
function calcAngleDeg(p1, p2) {
var p1x = p1.x ? p1.x : p1.cx;
var p1y = p1.y ? p1.y : p1.cy;
return (Math.atan2(p2.y - p1y, p2.x - p1x) * 180) / Math.PI;
}
}
return {
strict: 'A',
link: SvgDirectiveLink,
};
}
angular.module('svgApp').directive('svgDirective', SvgDirective);
})();