diff --git a/package.json b/package.json index f849f7ad1..378244797 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "build:bundle": "node build/build.js", "build:lib": "npx tsc -m ES2015 --outDir lib && node build/processLib.js", "watch:bundle": "node build/build.js --watch", - "watch:lib": "npx tsc -w -m ES2015 --outDir lib", + "watch:lib": "npx tsc-watch -m ES2015 --outDir lib --synchronousWatchDirectory --onSuccess \"node build/processLib.js\"", "test": "npx jest --config test/ut/jest.config.js", "lint": "npx eslint src/**/*.ts" }, diff --git a/src/graphic/helper/roundSector.ts b/src/graphic/helper/roundSector.ts index f11b6b8b9..42128502b 100644 --- a/src/graphic/helper/roundSector.ts +++ b/src/graphic/helper/roundSector.ts @@ -1,4 +1,5 @@ import PathProxy, { normalizeArcAngles } from '../../core/PathProxy'; +import { isArray } from '../../core/util'; const PI = Math.PI; const PI2 = PI * 2; @@ -12,31 +13,22 @@ const mathMax = Math.max; const mathMin = Math.min; const e = 1e-4; -type CornerTangents = { - cx: number - cy: number - x0: number - y0: number - x1: number - y1: number -}; - function intersect( x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number ): [number, number] { - const x10 = x1 - x0; - const y10 = y1 - y0; - const x32 = x3 - x2; - const y32 = y3 - y2; - let t = y32 * x10 - x32 * y10; + 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 = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t; - return [x0 + t * x10, y0 + t * y10]; + t = (dx32 * (y0 - y2) - dy32 * (x0 - x2)) / t; + return [x0 + t * dx10, y0 + t * dy10]; } // Compute perpendicular offset line of length rc. @@ -45,7 +37,7 @@ function computeCornerTangents( x1: number, y1: number, radius: number, cr: number, clockwise: boolean -): CornerTangents { +) { const x01 = x0 - x1; const y01 = y0 - y1; const lo = (clockwise ? cr : -cr) / mathSqrt(x01 * x01 + y01 * y01); @@ -89,6 +81,38 @@ function computeCornerTangents( }; } +// 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 @@ -97,8 +121,7 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { clockwise?: boolean, r?: number, r0?: number, - cornerRadius?: number, - innerCornerRadius?: number + cornerRadius?: number | number[] }) { let radius = mathMax(shape.r, 0); let innerRadius = mathMax(shape.r0 || 0, 0); @@ -123,8 +146,7 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { } const clockwise = !!shape.clockwise; - const startAngle = shape.startAngle; - const endAngle = shape.endAngle; + const { startAngle, endAngle, cx, cy, cornerRadius } = shape; // PENDING: whether normalizing angles is required? let arc: number; @@ -138,11 +160,6 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { arc = mathAbs(tmpAngles[0] - tmpAngles[1]); } - const cx = shape.cx; - const cy = shape.cy; - const cornerRadius = shape.cornerRadius || 0; - const innerCornerRadius = shape.innerCornerRadius || 0; - // is a point if (!(radius > e)) { ctx.moveTo(cx, cy); @@ -165,11 +182,24 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { } // is a circular or annular sector else { + let icrStart; + let icrEnd; + let ocrStart; + let ocrEnd; + if (cornerRadius) { + [icrStart, icrEnd, ocrStart, ocrEnd] = normalizeCornerRadius(cornerRadius); + } + const halfRd = mathAbs(radius - innerRadius) / 2; - const cr = mathMin(halfRd, cornerRadius); - const icr = mathMin(halfRd, innerCornerRadius); - let cr0 = icr; - let cr1 = cr; + const ocrs = mathMin(halfRd, ocrStart); + const ocre = mathMin(halfRd, ocrEnd); + const icrs = mathMin(halfRd, icrStart); + const icre = mathMin(halfRd, icrEnd); + + const ocrMax = mathMax(ocrs, ocre); + const icrMax = mathMax(icrs, icre); + let limitedOcrMax = ocrMax; + let limitedIcrMax = icrMax; const xrs = radius * mathCos(startAngle); const yrs = radius * mathSin(startAngle); @@ -182,7 +212,7 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { let yirs; // draw corner radius - if (cr > e || icr > e) { + if (ocrMax > e || icrMax > e) { xre = radius * mathCos(endAngle); yre = radius * mathSin(endAngle); xirs = innerRadius * mathCos(startAngle); @@ -200,8 +230,8 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { 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]); - cr0 = mathMin(icr, (innerRadius - b) / (a - 1)); - cr1 = mathMin(cr, (radius - b) / (a + 1)); + limitedOcrMax = mathMin(ocrMax, (radius - b) / (a + 1)); + limitedIcrMax = mathMin(icrMax, (innerRadius - b) / (a - 1)); } } } @@ -211,25 +241,27 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { ctx.moveTo(cx + xrs, cy + yrs); } // the outer ring has corners - else if (cr1 > e) { - const ct0 = computeCornerTangents(xirs, yirs, xrs, yrs, radius, cr1, clockwise); - const ct1 = computeCornerTangents(xre, yre, xire, yire, radius, cr1, clockwise); + 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 (cr1 < cr) { + if (limitedOcrMax < ocrMax && crStart === crEnd) { // eslint-disable-next-line max-len - ctx.arc(cx + ct0.cx, cy + ct0.cy, cr1, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise); + 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 - ctx.arc(cx + ct0.cx, cy + ct0.cy, cr1, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise); + 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 - ctx.arc(cx + ct1.cx, cy + ct1.cy, cr1, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise); + 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 @@ -243,24 +275,26 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: { ctx.lineTo(cx + xire, cy + yire); } // the inner ring has corners - else if (cr0 > e) { - const ct0 = computeCornerTangents(xire, yire, xre, yre, innerRadius, -cr0, clockwise); - const ct1 = computeCornerTangents(xrs, yrs, xirs, yirs, innerRadius, -cr0, clockwise); + 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 (cr0 < icr) { + if (limitedIcrMax < icrMax && crStart === crEnd) { // eslint-disable-next-line max-len - ctx.arc(cx + ct0.cx, cy + ct0.cy, cr0, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise); + 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 - ctx.arc(cx + ct0.cx, cy + ct0.cy, cr0, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise); + 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 - ctx.arc(cx + ct1.cx, cy + ct1.cy, cr0, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise); + 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 diff --git a/src/graphic/shape/Sector.ts b/src/graphic/shape/Sector.ts index 0b3f6cdfb..96550325f 100644 --- a/src/graphic/shape/Sector.ts +++ b/src/graphic/shape/Sector.ts @@ -9,8 +9,20 @@ export class SectorShape { startAngle = 0 endAngle = Math.PI * 2 clockwise = true - cornerRadius = 0 - innerCornerRadius = 0 + /** + * Corner radius of sector + * + * clockwise, from inside to outside, four corners are + * inner start -> inner end + * outer start -> outer end + * + * 5 => [5, 5, 5, 5] + * [5] => [5, 5, 0, 0] + * [5, 10] => [5, 5, 10, 10] + * [5, 10, 15] => [5, 10, 15, 15] + * [5, 10, 15, 20] => [5, 10, 15, 20] + */ + cornerRadius: number | number[] = 0 } export interface SectorProps extends PathProps { diff --git a/test/sector.html b/test/sector.html index 791187e62..e0930fea3 100644 --- a/test/sector.html +++ b/test/sector.html @@ -24,7 +24,9 @@ import * as zrender from '../index.js'; // 初始化zrender - var zr = zrender.init(document.getElementById('main')); + var zr = zrender.init(document.getElementById('main'), { + renderer: window.__ZRENDER__DEFAULT__RENDERER__ + }); zr.add(new zrender.Sector({ position: [100, 100], @@ -93,8 +95,54 @@ endAngle: 0, r0: 50, r: 100, - cornerRadius: 10, - innerCornerRadius: 10 + // cornerRadius: 10, + // innerCornerRadius: 10, + cornerRadius: [10, 10] + } + })); + + zr.add(new zrender.Sector({ + position: [400, 150], + scale: [1, 1], + style: { + stroke: 'black' + }, + shape: { + startAngle: Math.PI * -160 / 180, + endAngle: Math.PI * -20 / 180, + r0: 50, + r: 140, + cornerRadius: [5, 15, 0, 70], + // clockwise: false + } + })); + + zr.add(new zrender.Sector({ + position: [400, 350], + scale: [1, 1], + style: { + stroke: 'black' + }, + shape: { + startAngle: Math.PI * -160 / 180, + endAngle: Math.PI * -20 / 180, + r0: 50, + r: 140, + cornerRadius: [0, 0, 0, 20] + } + })); + + zr.add(new zrender.Sector({ + position: [400, 550], + scale: [1, 1], + style: { + stroke: 'black' + }, + shape: { + startAngle: Math.PI * -160 / 180, + endAngle: Math.PI * -20 / 180, + r: 140, + cornerRadius: [0, 0, 35, 20] } }));