Skip to content

Instantly share code, notes, and snippets.

@jose-mdz
Last active June 23, 2024 08:04
Show Gist options
  • Save jose-mdz/ed70f7d5677f6b9df63ee904a6a33743 to your computer and use it in GitHub Desktop.
Save jose-mdz/ed70f7d5677f6b9df63ee904a6a33743 to your computer and use it in GitHub Desktop.
Draw round corners path in JavaScript

Round Corner Path

[x] Tested

Usage

const canvas = document.getElementById('myCanvas');
const context = canvas.getContext('2d');
const points = [
  {x: 100, y: 100},
  {x: 100, y: 200},
  {x: 200, y: 200},
  {x: 200, y: 100}
  ];
roundCornersPath(points, context, 10);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Round Corners Path</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400"></canvas>
<script>
function roundCornersPath(points, c, radius = 15) {
function distance(a, b) {
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
}
function direction(a, b, c) {
const segment1Horizontal = a.y === b.y;
const segment1Vertical = a.x === b.x;
const segment2Horizontal = b.y === c.y;
const segment2Vertical = b.x === c.x;
if ((a.x === b.x && b.x === c.x) || (a.y === b.y && b.y === c.y)) {
return 'none';
}else if (!(segment1Vertical || segment1Horizontal) ||
!(segment2Vertical || segment2Horizontal)) {
return 'unknown';
}else if (segment1Horizontal && segment2Vertical) {
return c.y > b.y ? 's' : 'n';
}else{
return c.x > b.x ? 'e' : 'w';
}
}
function simplifyPath(points) {
if (points.length <= 2) {
return points;
}
const r = [points[0]];
for (let i = 1; i < points.length; i++) {
const cur = points[i];
if (i === (points.length - 1)) {
r.push(cur);
}else if(direction(points[i - 1], cur, points[i + 1]) !== 'none'){
r.push(cur);
}
}
return r;
}
function trace(points, radius){
if (points.length <= 1) return;
c.beginPath();
c.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1],
current = points[i], { x, y } = current,
next = points[i + 1];
if (next) {
const d1 = distance(prev, current), d2 = distance(current, next),
r2 = radius * 2, r = d1 < r2 || d2 < r2 ? Math.min(d1 / 2, d2 / 2) : radius,
fromW = prev.x < x, fromN = prev.y < y;
switch (direction(prev, current, next)) {
case 's':
c.lineTo(fromW ? x - r : x + r, y);
c.quadraticCurveTo(x, y, x, y + r);
break;
case 'n':
c.lineTo(fromW ? x - r : x + r, y);
c.quadraticCurveTo(x, y, x, y - r);
break;
case 'e':
c.lineTo(x, fromN ? y - r : y + r);
c.quadraticCurveTo(x, y, x + r, y);
break;
case 'w':
c.lineTo(x, fromN ? y - r : y + r);
c.quadraticCurveTo(x, y, x - r, y);
break;
default:
c.lineTo(x, y);
break;
}
}else {
c.lineTo(current.x, current.y);
}
}
c.stroke();
}
trace(simplifyPath(points), radius);
}
const canvas = document.getElementById('myCanvas');
const context = canvas.getContext('2d');
const points = [
{x: 100, y: 100},
{x: 100, y: 200},
{x: 200, y: 200},
{x: 200, y: 100},
{x: 300, y: 100},
{x: 300, y: 200},
];
roundCornersPath(points, context);
</script>
</body>
</html>
export type BendDirection = 'n' | 'e' | 's' | 'w' | 'unknown' | 'none';
export interface Point {
x: number;
y: number;
}
/**
* Returns the distance between the points
*/
function distance(a: Point, b: Point): number{
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
}
/**
* Pass three points representing two contiguous line segments
* If they form a straight horizontal or vertical line, `none` is returned
* In case they are orthogonal, result indicates the bend direction
* Any other case the result is `unknown`
* @param a
* @param b
* @param c
*/
function getBend(a: Point, b: Point, c: Point): BendDirection {
const equalX = a.x === b.x && b.x === c.x;
const equalY = a.y === b.y && b.y === c.y;
const segment1Horizontal = a.y === b.y;
const segment1Vertical = a.x === b.x;
const segment2Horizontal = b.y === c.y;
const segment2Vertical = b.x === c.x;
if( equalX || equalY ) {
return 'none';
}
if(
!(segment1Vertical || segment1Horizontal) ||
!(segment2Vertical || segment2Horizontal)
) {
return 'unknown';
}
if(segment1Horizontal && segment2Vertical) {
return c.y > b.y ? 's' : 'n';
}else if(segment1Vertical && segment2Horizontal) {
return c.x > b.x ? 'e' : 'w';
}
throw new Error('Nope');
}
/**
* Removes unnecessary points, where they form part of a straight vertical or horizontal line
* @param points
*/
function simplifyPath(points: Point[]): Point[]{
if(points.length <= 2){
return points;
}
const r: Point[] = [points[0]];
for(let i = 1; i < points.length; i++){
const cur = points[i];
if(i === (points.length - 1)) {
r.push(cur);
break;
}
const prev = points[i - 1];
const next = points[i + 1];
const bend = getBend(prev, cur, next);
if(bend !== 'none') {
r.push(cur);
}
}
return r;
}
/**
* Draws a round path
* @param points
* @param c
* @param radius
*/
export function roundCornersPath(points: Point[], c: CanvasRenderingContext2D, radius: number = 10){
points = simplifyPath(points);
if(points.length <= 1) {
return;
}
c.beginPath();
let prev = points[0];
c.moveTo(prev.x, prev.y);
for(let i = 1; i < points.length; i++){
const current = points[i];
const {x, y} = current;
const next: null | Point = points[i + 1] || null;
if(next) {
const b = getBend(prev, current, next);
const d1 = distance(prev, current);
const d2 = distance(current, next);
const r2 = radius * 2;
const r = d1 < r2 || d2 < r2 ? Math.min(d1/2, d2/2) : radius;
const fromW = prev.x < x;
const fromN = prev.y < y;
switch (b) {
case 's':
c.lineTo(fromW ? x - r : x + r, y);
c.quadraticCurveTo(x, y, x, y + r);
break;
case 'n':
c.lineTo(fromW ? x - r : x + r, y);
c.quadraticCurveTo(x, y, x, y - r, );
break;
case 'e':
c.lineTo(x, fromN ? y - r : y + r );
c.quadraticCurveTo(x, y, x + r, y);
break;
case 'w':
c.lineTo(x, fromN ? y - r : y + r );
c.quadraticCurveTo(x, y, x - r, y);
break;
default:
c.lineTo(x, y);
break;
}
}else{
c.lineTo(current.x, current.y);
}
prev = current;
}
c.stroke();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment