7ff9be9edaf7110485153823d06f598067f894bf3e5cbbd6313a92f615528967fe2a1d332c2f7f17c59cfe9d86b900fb7c5b8abe706d6a514fe67ce96e79b6 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import PathProxy from '../../core/PathProxy';
  2. import { isArray } from '../../core/util';
  3. const PI = Math.PI;
  4. const PI2 = PI * 2;
  5. const mathSin = Math.sin;
  6. const mathCos = Math.cos;
  7. const mathACos = Math.acos;
  8. const mathATan2 = Math.atan2;
  9. const mathAbs = Math.abs;
  10. const mathSqrt = Math.sqrt;
  11. const mathMax = Math.max;
  12. const mathMin = Math.min;
  13. const e = 1e-4;
  14. function intersect(
  15. x0: number, y0: number,
  16. x1: number, y1: number,
  17. x2: number, y2: number,
  18. x3: number, y3: number
  19. ): [number, number] {
  20. const dx10 = x1 - x0;
  21. const dy10 = y1 - y0;
  22. const dx32 = x3 - x2;
  23. const dy32 = y3 - y2;
  24. let t = dy32 * dx10 - dx32 * dy10;
  25. if (t * t < e) {
  26. return;
  27. }
  28. t = (dx32 * (y0 - y2) - dy32 * (x0 - x2)) / t;
  29. return [x0 + t * dx10, y0 + t * dy10];
  30. }
  31. // Compute perpendicular offset line of length rc.
  32. function computeCornerTangents(
  33. x0: number, y0: number,
  34. x1: number, y1: number,
  35. radius: number, cr: number,
  36. clockwise: boolean
  37. ) {
  38. const x01 = x0 - x1;
  39. const y01 = y0 - y1;
  40. const lo = (clockwise ? cr : -cr) / mathSqrt(x01 * x01 + y01 * y01);
  41. const ox = lo * y01;
  42. const oy = -lo * x01;
  43. const x11 = x0 + ox;
  44. const y11 = y0 + oy;
  45. const x10 = x1 + ox;
  46. const y10 = y1 + oy;
  47. const x00 = (x11 + x10) / 2;
  48. const y00 = (y11 + y10) / 2;
  49. const dx = x10 - x11;
  50. const dy = y10 - y11;
  51. const d2 = dx * dx + dy * dy;
  52. const r = radius - cr;
  53. const s = x11 * y10 - x10 * y11;
  54. const d = (dy < 0 ? -1 : 1) * mathSqrt(mathMax(0, r * r * d2 - s * s));
  55. let cx0 = (s * dy - dx * d) / d2;
  56. let cy0 = (-s * dx - dy * d) / d2;
  57. const cx1 = (s * dy + dx * d) / d2;
  58. const cy1 = (-s * dx + dy * d) / d2;
  59. const dx0 = cx0 - x00;
  60. const dy0 = cy0 - y00;
  61. const dx1 = cx1 - x00;
  62. const dy1 = cy1 - y00;
  63. // Pick the closer of the two intersection points
  64. // TODO: Is there a faster way to determine which intersection to use?
  65. if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) {
  66. cx0 = cx1;
  67. cy0 = cy1;
  68. }
  69. return {
  70. cx: cx0,
  71. cy: cy0,
  72. x0: -ox,
  73. y0: -oy,
  74. x1: cx0 * (radius / r - 1),
  75. y1: cy0 * (radius / r - 1)
  76. };
  77. }
  78. // For compatibility, don't use normalizeCssArray
  79. // 5 represents [5, 5, 5, 5]
  80. // [5] represents [5, 5, 0, 0]
  81. // [5, 10] represents [5, 5, 10, 10]
  82. // [5, 10, 15] represents [5, 10, 15, 15]
  83. // [5, 10, 15, 20] represents [5, 10, 15, 20]
  84. function normalizeCornerRadius(cr: number | number[]): number[] {
  85. let arr: number[];
  86. if (isArray(cr)) {
  87. const len = cr.length;
  88. if (!len) {
  89. return cr as number[];
  90. }
  91. if (len === 1) {
  92. arr = [cr[0], cr[0], 0, 0];
  93. }
  94. else if (len === 2) {
  95. arr = [cr[0], cr[0], cr[1], cr[1]];
  96. }
  97. else if (len === 3) {
  98. arr = cr.concat(cr[2]);
  99. }
  100. else {
  101. arr = cr;
  102. }
  103. }
  104. else {
  105. arr = [cr, cr, cr, cr];
  106. }
  107. return arr;
  108. }
  109. export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: {
  110. cx: number
  111. cy: number
  112. startAngle: number
  113. endAngle: number
  114. clockwise?: boolean,
  115. r?: number,
  116. r0?: number,
  117. cornerRadius?: number | number[]
  118. }) {
  119. let radius = mathMax(shape.r, 0);
  120. let innerRadius = mathMax(shape.r0 || 0, 0);
  121. const hasRadius = radius > 0;
  122. const hasInnerRadius = innerRadius > 0;
  123. if (!hasRadius && !hasInnerRadius) {
  124. return;
  125. }
  126. if (!hasRadius) {
  127. // use innerRadius as radius if no radius
  128. radius = innerRadius;
  129. innerRadius = 0;
  130. }
  131. if (innerRadius > radius) {
  132. // swap, ensure that radius is always larger than innerRadius
  133. const tmp = radius;
  134. radius = innerRadius;
  135. innerRadius = tmp;
  136. }
  137. const { startAngle, endAngle } = shape;
  138. if (isNaN(startAngle) || isNaN(endAngle)) {
  139. return;
  140. }
  141. const { cx, cy } = shape;
  142. const clockwise = !!shape.clockwise;
  143. let arc = mathAbs(endAngle - startAngle);
  144. const mod = arc > PI2 && arc % PI2;
  145. mod > e && (arc = mod);
  146. // is a point
  147. if (!(radius > e)) {
  148. ctx.moveTo(cx, cy);
  149. }
  150. // is a circle or annulus
  151. else if (arc > PI2 - e) {
  152. ctx.moveTo(
  153. cx + radius * mathCos(startAngle),
  154. cy + radius * mathSin(startAngle)
  155. );
  156. ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise);
  157. if (innerRadius > e) {
  158. ctx.moveTo(
  159. cx + innerRadius * mathCos(endAngle),
  160. cy + innerRadius * mathSin(endAngle)
  161. );
  162. ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise);
  163. }
  164. }
  165. // is a circular or annular sector
  166. else {
  167. let icrStart;
  168. let icrEnd;
  169. let ocrStart;
  170. let ocrEnd;
  171. let ocrs;
  172. let ocre;
  173. let icrs;
  174. let icre;
  175. let ocrMax;
  176. let icrMax;
  177. let limitedOcrMax;
  178. let limitedIcrMax;
  179. let xre;
  180. let yre;
  181. let xirs;
  182. let yirs;
  183. const xrs = radius * mathCos(startAngle);
  184. const yrs = radius * mathSin(startAngle);
  185. const xire = innerRadius * mathCos(endAngle);
  186. const yire = innerRadius * mathSin(endAngle);
  187. const hasArc = arc > e;
  188. if (hasArc) {
  189. const cornerRadius = shape.cornerRadius;
  190. if (cornerRadius) {
  191. [icrStart, icrEnd, ocrStart, ocrEnd] = normalizeCornerRadius(cornerRadius);
  192. }
  193. const halfRd = mathAbs(radius - innerRadius) / 2;
  194. ocrs = mathMin(halfRd, ocrStart);
  195. ocre = mathMin(halfRd, ocrEnd);
  196. icrs = mathMin(halfRd, icrStart);
  197. icre = mathMin(halfRd, icrEnd);
  198. limitedOcrMax = ocrMax = mathMax(ocrs, ocre);
  199. limitedIcrMax = icrMax = mathMax(icrs, icre);
  200. // draw corner radius
  201. if (ocrMax > e || icrMax > e) {
  202. xre = radius * mathCos(endAngle);
  203. yre = radius * mathSin(endAngle);
  204. xirs = innerRadius * mathCos(startAngle);
  205. yirs = innerRadius * mathSin(startAngle);
  206. // restrict the max value of corner radius
  207. if (arc < PI) {
  208. const it = intersect(xrs, yrs, xirs, yirs, xre, yre, xire, yire);
  209. if (it) {
  210. const x0 = xrs - it[0];
  211. const y0 = yrs - it[1];
  212. const x1 = xre - it[0];
  213. const y1 = yre - it[1];
  214. const a = 1 / mathSin(
  215. // eslint-disable-next-line max-len
  216. mathACos((x0 * x1 + y0 * y1) / (mathSqrt(x0 * x0 + y0 * y0) * mathSqrt(x1 * x1 + y1 * y1))) / 2
  217. );
  218. const b = mathSqrt(it[0] * it[0] + it[1] * it[1]);
  219. limitedOcrMax = mathMin(ocrMax, (radius - b) / (a + 1));
  220. limitedIcrMax = mathMin(icrMax, (innerRadius - b) / (a - 1));
  221. }
  222. }
  223. }
  224. }
  225. // the sector is collapsed to a line
  226. if (!hasArc) {
  227. ctx.moveTo(cx + xrs, cy + yrs);
  228. }
  229. // the outer ring has corners
  230. else if (limitedOcrMax > e) {
  231. const crStart = mathMin(ocrStart, limitedOcrMax);
  232. const crEnd = mathMin(ocrEnd, limitedOcrMax);
  233. const ct0 = computeCornerTangents(xirs, yirs, xrs, yrs, radius, crStart, clockwise);
  234. const ct1 = computeCornerTangents(xre, yre, xire, yire, radius, crEnd, clockwise);
  235. ctx.moveTo(cx + ct0.cx + ct0.x0, cy + ct0.cy + ct0.y0);
  236. // Have the corners merged?
  237. if (limitedOcrMax < ocrMax && crStart === crEnd) {
  238. // eslint-disable-next-line max-len
  239. ctx.arc(cx + ct0.cx, cy + ct0.cy, limitedOcrMax, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise);
  240. }
  241. else {
  242. // draw the two corners and the ring
  243. // eslint-disable-next-line max-len
  244. crStart > 0 && ctx.arc(cx + ct0.cx, cy + ct0.cy, crStart, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise);
  245. // eslint-disable-next-line max-len
  246. ctx.arc(cx, cy, radius, mathATan2(ct0.cy + ct0.y1, ct0.cx + ct0.x1), mathATan2(ct1.cy + ct1.y1, ct1.cx + ct1.x1), !clockwise);
  247. // eslint-disable-next-line max-len
  248. crEnd > 0 && ctx.arc(cx + ct1.cx, cy + ct1.cy, crEnd, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise);
  249. }
  250. }
  251. // the outer ring is a circular arc
  252. else {
  253. ctx.moveTo(cx + xrs, cy + yrs);
  254. ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise);
  255. }
  256. // no inner ring, is a circular sector
  257. if (!(innerRadius > e) || !hasArc) {
  258. ctx.lineTo(cx + xire, cy + yire);
  259. }
  260. // the inner ring has corners
  261. else if (limitedIcrMax > e) {
  262. const crStart = mathMin(icrStart, limitedIcrMax);
  263. const crEnd = mathMin(icrEnd, limitedIcrMax);
  264. const ct0 = computeCornerTangents(xire, yire, xre, yre, innerRadius, -crEnd, clockwise);
  265. const ct1 = computeCornerTangents(xrs, yrs, xirs, yirs, innerRadius, -crStart, clockwise);
  266. ctx.lineTo(cx + ct0.cx + ct0.x0, cy + ct0.cy + ct0.y0);
  267. // Have the corners merged?
  268. if (limitedIcrMax < icrMax && crStart === crEnd) {
  269. // eslint-disable-next-line max-len
  270. ctx.arc(cx + ct0.cx, cy + ct0.cy, limitedIcrMax, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise);
  271. }
  272. // draw the two corners and the ring
  273. else {
  274. // eslint-disable-next-line max-len
  275. crEnd > 0 && ctx.arc(cx + ct0.cx, cy + ct0.cy, crEnd, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise);
  276. // eslint-disable-next-line max-len
  277. ctx.arc(cx, cy, innerRadius, mathATan2(ct0.cy + ct0.y1, ct0.cx + ct0.x1), mathATan2(ct1.cy + ct1.y1, ct1.cx + ct1.x1), clockwise);
  278. // eslint-disable-next-line max-len
  279. crStart > 0 && ctx.arc(cx + ct1.cx, cy + ct1.cy, crStart, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise);
  280. }
  281. }
  282. // the inner ring is just a circular arc
  283. else {
  284. // FIXME: if no lineTo, svg renderer will perform an abnormal drawing behavior.
  285. ctx.lineTo(cx + xire, cy + yire);
  286. ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise);
  287. }
  288. }
  289. ctx.closePath();
  290. }