import PathProxy from '../../core/PathProxy'; import { isArray } from '../../core/util'; const PI = Math.PI; const PI2 = PI * 2; const mathSin = Math.sin; const mathCos = Math.cos; const mathACos = Math.acos; const mathATan2 = Math.atan2; const mathAbs = Math.abs; const mathSqrt = Math.sqrt; const mathMax = Math.max; const mathMin = Math.min; const e = 1e-4; function intersect( x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number ): [number, number] { const dx10 = x1 - x0; const dy10 = y1 - y0; const dx32 = x3 - x2; const dy32 = y3 - y2; let t = dy32 * dx10 - dx32 * dy10; if (t * t < e) { return; } t = (dx32 * (y0 - y2) - dy32 * (x0 - x2)) / t; return [x0 + t * dx10, y0 + t * dy10]; } // Compute perpendicular offset line of length rc. function computeCornerTangents( x0: number, y0: number, x1: number, y1: number, radius: number, cr: number, clockwise: boolean ) { const x01 = x0 - x1; const y01 = y0 - y1; const lo = (clockwise ? cr : -cr) / mathSqrt(x01 * x01 + y01 * y01); const ox = lo * y01; const oy = -lo * x01; const x11 = x0 + ox; const y11 = y0 + oy; const x10 = x1 + ox; const y10 = y1 + oy; const x00 = (x11 + x10) / 2; const y00 = (y11 + y10) / 2; const dx = x10 - x11; const dy = y10 - y11; const d2 = dx * dx + dy * dy; const r = radius - cr; const s = x11 * y10 - x10 * y11; const d = (dy < 0 ? -1 : 1) * mathSqrt(mathMax(0, r * r * d2 - s * s)); let cx0 = (s * dy - dx * d) / d2; let cy0 = (-s * dx - dy * d) / d2; const cx1 = (s * dy + dx * d) / d2; const cy1 = (-s * dx + dy * d) / d2; const dx0 = cx0 - x00; const dy0 = cy0 - y00; const dx1 = cx1 - x00; const dy1 = cy1 - y00; // Pick the closer of the two intersection points // TODO: Is there a faster way to determine which intersection to use? if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) { cx0 = cx1; cy0 = cy1; } return { cx: cx0, cy: cy0, x0: -ox, y0: -oy, x1: cx0 * (radius / r - 1), y1: cy0 * (radius / r - 1) }; } // For compatibility, don't use normalizeCssArray // 5 represents [5, 5, 5, 5] // [5] represents [5, 5, 0, 0] // [5, 10] represents [5, 5, 10, 10] // [5, 10, 15] represents [5, 10, 15, 15] // [5, 10, 15, 20] represents [5, 10, 15, 20] function normalizeCornerRadius(cr: number | number[]): number[] { let arr: number[]; if (isArray(cr)) { const len = cr.length; if (!len) { return cr as number[]; } if (len === 1) { arr = [cr[0], cr[0], 0, 0]; } else if (len === 2) { arr = [cr[0], cr[0], cr[1], cr[1]]; } else if (len === 3) { arr = cr.concat(cr[2]); } else { arr = cr; } } else { arr = [cr, cr, cr, cr]; } return arr; } export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { cx: number cy: number startAngle: number endAngle: number clockwise?: boolean, r?: number, r0?: number, cornerRadius?: number | number[] }) { let radius = mathMax(shape.r, 0); let innerRadius = mathMax(shape.r0 || 0, 0); const hasRadius = radius > 0; const hasInnerRadius = innerRadius > 0; if (!hasRadius && !hasInnerRadius) { return; } if (!hasRadius) { // use innerRadius as radius if no radius radius = innerRadius; innerRadius = 0; } if (innerRadius > radius) { // swap, ensure that radius is always larger than innerRadius const tmp = radius; radius = innerRadius; innerRadius = tmp; } const { startAngle, endAngle } = shape; if (isNaN(startAngle) || isNaN(endAngle)) { return; } const { cx, cy } = shape; const clockwise = !!shape.clockwise; let arc = mathAbs(endAngle - startAngle); const mod = arc > PI2 && arc % PI2; mod > e && (arc = mod); // is a point if (!(radius > e)) { ctx.moveTo(cx, cy); } // is a circle or annulus else if (arc > PI2 - e) { ctx.moveTo( cx + radius * mathCos(startAngle), cy + radius * mathSin(startAngle) ); ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise); if (innerRadius > e) { ctx.moveTo( cx + innerRadius * mathCos(endAngle), cy + innerRadius * mathSin(endAngle) ); ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise); } } // is a circular or annular sector else { let icrStart; let icrEnd; let ocrStart; let ocrEnd; let ocrs; let ocre; let icrs; let icre; let ocrMax; let icrMax; let limitedOcrMax; let limitedIcrMax; let xre; let yre; let xirs; let yirs; const xrs = radius * mathCos(startAngle); const yrs = radius * mathSin(startAngle); const xire = innerRadius * mathCos(endAngle); const yire = innerRadius * mathSin(endAngle); const hasArc = arc > e; if (hasArc) { const cornerRadius = shape.cornerRadius; if (cornerRadius) { [icrStart, icrEnd, ocrStart, ocrEnd] = normalizeCornerRadius(cornerRadius); } const halfRd = mathAbs(radius - innerRadius) / 2; ocrs = mathMin(halfRd, ocrStart); ocre = mathMin(halfRd, ocrEnd); icrs = mathMin(halfRd, icrStart); icre = mathMin(halfRd, icrEnd); limitedOcrMax = ocrMax = mathMax(ocrs, ocre); limitedIcrMax = icrMax = mathMax(icrs, icre); // draw corner radius if (ocrMax > e || icrMax > e) { xre = radius * mathCos(endAngle); yre = radius * mathSin(endAngle); xirs = innerRadius * mathCos(startAngle); yirs = innerRadius * mathSin(startAngle); // restrict the max value of corner radius if (arc < PI) { const it = intersect(xrs, yrs, xirs, yirs, xre, yre, xire, yire); if (it) { const x0 = xrs - it[0]; const y0 = yrs - it[1]; const x1 = xre - it[0]; const y1 = yre - it[1]; const a = 1 / mathSin( // eslint-disable-next-line max-len mathACos((x0 * x1 + y0 * y1) / (mathSqrt(x0 * x0 + y0 * y0) * mathSqrt(x1 * x1 + y1 * y1))) / 2 ); const b = mathSqrt(it[0] * it[0] + it[1] * it[1]); limitedOcrMax = mathMin(ocrMax, (radius - b) / (a + 1)); limitedIcrMax = mathMin(icrMax, (innerRadius - b) / (a - 1)); } } } } // the sector is collapsed to a line if (!hasArc) { ctx.moveTo(cx + xrs, cy + yrs); } // the outer ring has corners else if (limitedOcrMax > e) { const crStart = mathMin(ocrStart, limitedOcrMax); const crEnd = mathMin(ocrEnd, limitedOcrMax); const ct0 = computeCornerTangents(xirs, yirs, xrs, yrs, radius, crStart, clockwise); const ct1 = computeCornerTangents(xre, yre, xire, yire, radius, crEnd, clockwise); ctx.moveTo(cx + ct0.cx + ct0.x0, cy + ct0.cy + ct0.y0); // Have the corners merged? if (limitedOcrMax < ocrMax && crStart === crEnd) { // eslint-disable-next-line max-len ctx.arc(cx + ct0.cx, cy + ct0.cy, limitedOcrMax, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise); } else { // draw the two corners and the ring // eslint-disable-next-line max-len crStart > 0 && ctx.arc(cx + ct0.cx, cy + ct0.cy, crStart, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise); // eslint-disable-next-line max-len ctx.arc(cx, cy, radius, mathATan2(ct0.cy + ct0.y1, ct0.cx + ct0.x1), mathATan2(ct1.cy + ct1.y1, ct1.cx + ct1.x1), !clockwise); // eslint-disable-next-line max-len crEnd > 0 && ctx.arc(cx + ct1.cx, cy + ct1.cy, crEnd, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise); } } // the outer ring is a circular arc else { ctx.moveTo(cx + xrs, cy + yrs); ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise); } // no inner ring, is a circular sector if (!(innerRadius > e) || !hasArc) { ctx.lineTo(cx + xire, cy + yire); } // the inner ring has corners else if (limitedIcrMax > e) { const crStart = mathMin(icrStart, limitedIcrMax); const crEnd = mathMin(icrEnd, limitedIcrMax); const ct0 = computeCornerTangents(xire, yire, xre, yre, innerRadius, -crEnd, clockwise); const ct1 = computeCornerTangents(xrs, yrs, xirs, yirs, innerRadius, -crStart, clockwise); ctx.lineTo(cx + ct0.cx + ct0.x0, cy + ct0.cy + ct0.y0); // Have the corners merged? if (limitedIcrMax < icrMax && crStart === crEnd) { // eslint-disable-next-line max-len ctx.arc(cx + ct0.cx, cy + ct0.cy, limitedIcrMax, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise); } // draw the two corners and the ring else { // eslint-disable-next-line max-len crEnd > 0 && ctx.arc(cx + ct0.cx, cy + ct0.cy, crEnd, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise); // eslint-disable-next-line max-len ctx.arc(cx, cy, innerRadius, mathATan2(ct0.cy + ct0.y1, ct0.cx + ct0.x1), mathATan2(ct1.cy + ct1.y1, ct1.cx + ct1.x1), clockwise); // eslint-disable-next-line max-len crStart > 0 && ctx.arc(cx + ct1.cx, cy + ct1.cy, crStart, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise); } } // the inner ring is just a circular arc else { // FIXME: if no lineTo, svg renderer will perform an abnormal drawing behavior. ctx.lineTo(cx + xire, cy + yire); ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise); } } ctx.closePath(); }