<!DOCTYPE html>
<html ng-app="roundingApp">
<head>
<script data-require="angular.js@*" data-semver="1.3.1" src="//code.angularjs.org/1.3.1/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="rounding.js"></script>
<script src="script.js"></script>
</head>
<body ng-controller="RoundingCtrl">
<div rounding-test-case="M0 0 L 100 0 L100 100 L 0 100 Z"></div>
<div rounding-test-case="M0 0 L 100 0 L100 100 L 0 100"></div>
<div rounding-test-case="M0 0 L 100 0 L100 50 L 0 50 Z"></div>
<div rounding-test-case="M0 0 L 100 0 L50 100 Z"></div>
<div rounding-test-case="M0 0 L 100 0 L100 100"></div>
<div rounding-test-case="M0 0 L 20 100 L 40 0 L 60 100 L 80 0 L 100 100"></div>
<div rounding-test-case="M 0 0 C 25 0 50 50 100 50 L 0 100 L 25 50 Z"></div>
<div rounding-test-case="M 0 0 L 100 0 L 100 100 M 25 25 L 75 25 L 75 75"></div>
</body>
</html>
var roundingApp = angular.module('roundingApp', []);
roundingApp.directive('roundingTestCase', function() {
return {
restrict: "A",
scope: {
"roundingTestCase": "@"
},
replace: true,
template: ''
+'<div class="test-case-container">'
+' <svg width="120" height="120" viewBox="-10 -10 120 120">'
+' <path ng-attr-d="{{ roundedPath }}" fill="none" stroke="red" stroke-width="6" />'
+' <path ng-attr-d="{{ roundingTestCase }}" fill="none" stroke="black" stroke-width="1" />'
+' </svg>'
+' <input type="range" min="0" max="1" step="0.01" ng-model="config.rounding" />'
+' <div><label><input type="checkbox" ng-model="config.fractional" /> Fractional</label></div>'
+'</div>',
controller: function($scope){
var origPath = $scope.roundingTestCase;
$scope.config = {
rounding: .1,
fractional: false
};
$scope.$watch("config", function() {
$scope.roundedPath = roundPathCorners(
origPath,
$scope.config.fractional
? $scope.config.rounding
: $scope.config.rounding * 100,
$scope.config.fractional
);
}, true);
}
};
});
roundingApp.directive('myCustomer', function() {
return {
template: 'Foobat12'
};
})
roundingApp.controller(
"RoundingCtrl",
function RoundingCtrl($scope) {
$scope.foo = 10;
}
)
/* Styles go here */
.test-case-container {
display: inline-block;
border: 2px solid gray;
padding: .5em;
margin: 1em;
display: inline-block;
text-align: center;
}
.test-case-container svg {
display: block;
}
/*****************************************************************************
* *
* SVG Path Rounding Function *
* Copyright (C) 2014 Yona Appletree *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* *
*****************************************************************************/
/**
* SVG Path rounding function. Takes an input path string and outputs a path
* string where all line-line corners have been rounded. Only supports absolute
* commands at the moment.
*
* @param pathString The SVG input path
* @param radius The amount to round the corners, either a value in the SVG
* coordinate space, or, if useFractionalRadius is true, a value
* from 0 to 1.
* @param useFractionalRadius If true, the curve radius is expressed as a
* fraction of the distance between the point being curved and
* the previous and next points.
* @returns A new SVG path string with the rounding
*/
function roundPathCorners(pathString, radius, useFractionalRadius) {
function moveTowardsLength(movingPoint, targetPoint, amount) {
var width = (targetPoint.x - movingPoint.x);
var height = (targetPoint.y - movingPoint.y);
var distance = Math.sqrt(width*width + height*height);
return moveTowardsFractional(movingPoint, targetPoint, Math.min(1, amount / distance));
}
function moveTowardsFractional(movingPoint, targetPoint, fraction) {
return {
x: movingPoint.x + (targetPoint.x - movingPoint.x)*fraction,
y: movingPoint.y + (targetPoint.y - movingPoint.y)*fraction
};
}
// Adjusts the ending position of a command
function adjustCommand(cmd, newPoint) {
if (cmd.length > 2) {
cmd[cmd.length - 2] = newPoint.x;
cmd[cmd.length - 1] = newPoint.y;
}
}
// Gives an {x, y} object for a command's ending position
function pointForCommand(cmd) {
return {
x: parseFloat(cmd[cmd.length - 2]),
y: parseFloat(cmd[cmd.length - 1]),
};
}
// Split apart the path, handing concatonated letters and numbers
var pathParts = pathString
.split(/[,\s]/)
.reduce(function(parts, part){
var match = part.match("([a-zA-Z])(.+)");
if (match) {
parts.push(match[1]);
parts.push(match[2]);
} else {
parts.push(part);
}
return parts;
}, []);
// Group the commands with their arguments for easier handling
var commands = pathParts.reduce(function(commands, part) {
if (parseFloat(part) == part && commands.length) {
commands[commands.length - 1].push(part);
} else {
commands.push([part]);
}
return commands;
}, []);
// The resulting commands, also grouped
var resultCommands = [];
if (commands.length > 1) {
var startPoint = pointForCommand(commands[0]);
// Handle the close path case with a "virtual" closing line
var virtualCloseLine = null;
if (commands[commands.length - 1][0] == "Z" && commands[0].length > 2) {
virtualCloseLine = ["L", startPoint.x, startPoint.y];
commands[commands.length - 1] = virtualCloseLine;
}
// We always use the first command (but it may be mutated)
resultCommands.push(commands[0]);
for (var cmdIndex=1; cmdIndex < commands.length; cmdIndex++) {
var prevCmd = resultCommands[resultCommands.length - 1];
var curCmd = commands[cmdIndex];
// Handle closing case
var nextCmd = (curCmd == virtualCloseLine)
? commands[1]
: commands[cmdIndex + 1];
// Nasty logic to decide if this path is a candidite.
if (nextCmd && prevCmd && (prevCmd.length > 2) && curCmd[0] == "L" && nextCmd.length > 2 && nextCmd[0] == "L") {
// Calc the points we're dealing with
var prevPoint = pointForCommand(prevCmd);
var curPoint = pointForCommand(curCmd);
var nextPoint = pointForCommand(nextCmd);
// The start and end of the cuve are just our point moved towards the previous and next points, respectivly
var curveStart, curveEnd;
if (useFractionalRadius) {
curveStart = moveTowardsFractional(curPoint, prevCmd.origPoint || prevPoint, radius);
curveEnd = moveTowardsFractional(curPoint, nextCmd.origPoint || nextPoint, radius);
} else {
curveStart = moveTowardsLength(curPoint, prevPoint, radius);
curveEnd = moveTowardsLength(curPoint, nextPoint, radius);
}
// Adjust the current command and add it
adjustCommand(curCmd, curveStart);
curCmd.origPoint = curPoint;
resultCommands.push(curCmd);
// The curve control points are halfway between the start/end of the curve and
// the original point
var startControl = moveTowardsFractional(curveStart, curPoint, .5);
var endControl = moveTowardsFractional(curPoint, curveEnd, .5);
// Create the curve
var curveCmd = ["C", startControl.x, startControl.y, endControl.x, endControl.y, curveEnd.x, curveEnd.y];
// Save the original point for fractional calculations
curveCmd.origPoint = curPoint;
resultCommands.push(curveCmd);
} else {
// Pass through commands that don't qualify
resultCommands.push(curCmd);
}
}
// Fix up the starting point and restore the close path if the path was orignally closed
if (virtualCloseLine) {
var newStartPoint = pointForCommand(resultCommands[resultCommands.length-1]);
resultCommands.push(["Z"]);
adjustCommand(resultCommands[0], newStartPoint);
}
} else {
resultCommands = commands;
}
return resultCommands.reduce(function(str, c){ return str + c.join(" ") + " "; }, "");
}