|
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(); |
|
} |