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