round-polygon
Small, typed, dependency-free tool to round corners of any 2d-polygon provided by an array of { x, y }
points.
The algorithm prevents rounding overlaps, so if you pass an oversized radius, it won't break the shape, but instead calculate the maximum radius of each point, just like you expect.
Demo page
Installation
by npm:
npm i round-polygon
and then
import roundPolygon from "round-polygon"
or if you don't use npm you can import module from unpkg:
import roundPolygon from "https://unpkg.com/round-polygon@latest/dist/round-polygon.es.js"
or by script tag (with a link to IIFE module, which pollutes global scope with roundPolygon and getSegments functions) in your html page:
<script src="https://unpkg.com/round-polygon@latest/dist/round-polygon.iife.js"></script>
Usage
Types
if you use TypeScript you can also import input type "InitPoint" and output type "RoundedPoint"
import roundPolygon, { InitPoint, RoundedPoint } from "round-polygon"
let polygonToRound: InitPoint[],
roundedPolygon: RoundedPoint[],
radius: number
Input
roundPolygon function takes two arguments: an array of initial points and a radius
polygonToRound = [
{ x: 100, y: 0 },
{ x: 0, y: 150 },
{ x: 200, y: 150 },
{ x: 200, y: 0 },
{ x: 150, y: 200 },
]
radius = 20
roundedPolygon = roundPolygon(polygonToRound, radius)
To set a certain radius for a certain point, just add "r" property to the initial point. The radius passed as the argument to the function won't affect these points. Keep in mind that the algorithm rounds these points with higher priority.
{ x: 100, y: 0, r: 50 }
Output
a rounded point is an object with provided properties:
{
id: number, // index of the current point
x: number, // x-coordinate of the initial point
y: number, // y-coordinate of the initial point
offset: number, // length between the initial point and the start or the end of a rounding arc
angle: { // in radians
main: number, // the angle between the previous point, the current one, and the next one
prev: number, // the angle between prev-to-curr-line to x-Axis
next: number, // the angle between next-to-curr-line to x-Axis
bis: number, // the bisector angle to x-Axis
dir: 1 | -1 // whether clockwise (1) or counter-clockwise (-1) is the main angle direction (from the "prev" to the "next" angle)
},
arc: {
radius: number, // the rounding radius of the current point (might be less then provided as an argument (caused by rounding overlapping))
x: number, // x-coordinate of rounding center
y: number // y-coordinate of rounding center
},
in: {
length: number, // the length of prev-to-curr-line
x: number, // x-coordinate where the arc begins laying on prev-to-curr-line
y: number // y-coordinate where the arc begins laying on prev-to-curr-line
},
out: {
length: number, // the length of next-to-curr-line
x: number, // x-coordinate where arc ends laying on next-to-curr-line
y: number // y-coordinate where arc ends laying on next-to-curr-line
},
prev: {...}, // a getter, returns prev-indexed rounded point
next: {...} // a getter, returns next-indexed rounded point
}
Segments
You might need the rounded polygon provided with only { x, y }
points, so every arc should be divided by segments of stright lines. Given getSegments
function does that, it calculates coordinates of segmented arcs points. It takes array of rounded points, type of arc division (by LENGTH
of segments or by AMOUNT
of points) and a value to divide by corresponded to this type. It returns an array of { x, y }
points.
import roundPolygon, { getSegments, Point } from "round-polygon"
let segments: Point[]
roundedPolygon = roundPolygon(polygonToRound, radius)
segments = getSegments(roundedPolygon, "LENGTH", 20)
Summary
so the whole approach to draw a rounded shape using, for example, Canvas API looks like this:
// init
const
canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
polygonToRound = [
{ x: 100, y: 0, r: 60 },
{ x: 0, y: 150 },
{ x: 200, y: 150, r: 60 },
{ x: 200, y: 0 },
{ x: 150, y: 200 },
],
radius = 1000,
roundedPolygon = roundPolygon(polygonToRound, radius),
segments = getSegments(roundedPolygon, "LENGTH", 10)
// draw rounded shape
ctx.beginPath()
roundedPolygon.forEach((p, i) => {
!i && ctx.moveTo(p.in.x, p.in.y)
ctx.arcTo(p.x, p.y, p.out.x, p.out.y, p.arc.radius)
ctx.lineTo(p.next.in.x, p.next.in.y)
})
ctx.stroke()
// draw segments of rounded shape
segments.forEach((p) => {
ctx.beginPath()
ctx.arc(p.x, p.y, 2, 0, Math.PI*2)
ctx.fill()
})
Changelog
v0.6.6
- add
getSegments
utilite to calculate segments of rounded corner arc - cleaner code
v0.6.4
- common radius doesn't affect init-points with "r" = 0
- handle points overlapping, 0/PI radians main-angle point
v0.6.0
- added ability to provide a certain radius to a certain Point
- some bugs fixed
v0.5.9
- performance improvement (clean code)
v0.5.1
- first stable version
Upcoming
- input and output might be SVG path
- provide bezier curve estimations as an alternative to an arc output property