173 lines
6.7 KiB
JavaScript
173 lines
6.7 KiB
JavaScript
|
/*
|
||
|
* Source: http://plnkr.co/edit/kGnGGyoOCKil02k04snu?p=info
|
||
|
*/
|
||
|
|
||
|
/*****************************************************************************
|
||
|
* *
|
||
|
* 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(" ") + " "; }, "");
|
||
|
}
|