/* * 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(" ") + " "; }, ""); }