Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sector): enhance sector corner radius to support configuring any one corner. #865

Merged
merged 16 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
155 changes: 98 additions & 57 deletions src/graphic/helper/roundSector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { parsePercent } from '../../contain/text';
import PathProxy, { normalizeArcAngles } from '../../core/PathProxy';
import { isArray, map } from '../../core/util';

const PI = Math.PI;
const PI2 = PI * 2;
Expand All @@ -12,31 +14,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.
Expand All @@ -45,7 +38,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);
Expand Down Expand Up @@ -89,6 +82,48 @@ function computeCornerTangents(
};
}

function calcCircleCenter(x: number, y: number, r: number, angle: number) {
return {
x: x + r * mathCos(angle),
y: y + r * mathSin(angle)
};
}

// 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 | string | (number | string)[],
r0: number,
r: number
) {
let arr: (number | string)[];
if (isArray(cr)) {
const len = cr.length;
if (len === 4) {
arr = cr;
}
else if (len === 3) {
arr = cr.concat(cr[len - 1]);
}
else if (len === 2) {
arr = [cr[0], cr[0], cr[1], cr[1]];
}
else {
arr = [cr[0], cr[0], 0, 0];
}
}
else {
pissang marked this conversation as resolved.
Show resolved Hide resolved
arr = [cr, cr, cr, cr];
}
// use `r - r0` if the sector is annular
const dr = r0 ? r - r0 : r;
return map(arr, cr => parsePercent(cr, dr));
}

export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: {
cx: number
cy: number
Expand All @@ -97,8 +132,7 @@ export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: {
clockwise?: boolean,
r?: number,
r0?: number,
cornerRadius?: number,
innerCornerRadius?: number
cornerRadius?: number | string | (number | string)[]
}) {
let radius = mathMax(shape.r, 0);
let innerRadius = mathMax(shape.r0 || 0, 0);
Expand All @@ -123,8 +157,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;
Expand All @@ -138,38 +171,45 @@ 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;
let icrStart;
let icrEnd;
let ocrStart;
let ocrEnd;
if (cornerRadius) {
[icrStart, icrEnd, ocrStart, ocrEnd] = normalizeCornerRadius(cornerRadius, innerRadius, radius);
}
else {
icrStart = icrEnd = ocrStart = ocrEnd = 0;
}

// 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)
);
const { x, y } = calcCircleCenter(cx, cy, radius, startAngle);
ctx.moveTo(x, y);
ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise);

if (innerRadius > e) {
ctx.moveTo(
cx + innerRadius * mathCos(endAngle),
cy + innerRadius * mathSin(endAngle)
);
const { x, y } = calcCircleCenter(cx, cy, innerRadius, endAngle);
plainheart marked this conversation as resolved.
Show resolved Hide resolved
ctx.moveTo(x, y);
ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise);
}
}
// is a circular or annular sector
else {
const halfRd = mathAbs(radius - innerRadius) / 2;
const cr = mathMin(halfRd, cornerRadius);
const icr = mathMin(halfRd, innerCornerRadius);
let cr0 = icr;
let cr1 = cr;
let ocrs = mathMin(halfRd, ocrStart);
let ocre = mathMin(halfRd, ocrEnd);
let icrs = mathMin(halfRd, icrStart);
let icre = mathMin(halfRd, icrEnd);

let ocrMax = mathMax(ocrs, ocre);
let icrMax = mathMax(icrs, icre);
let limitedOcrMax = ocrMax;
let limitedIcrMax = icrMax;

const xrs = radius * mathCos(startAngle);
const yrs = radius * mathSin(startAngle);
Expand All @@ -182,7 +222,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);
Expand All @@ -200,8 +240,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));
}
}
}
Expand All @@ -211,25 +251,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) {
// 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);
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);
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
Expand All @@ -243,31 +285,30 @@ 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) {
// 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);
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);
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);
}
}
Expand Down
16 changes: 14 additions & 2 deletions src/graphic/shape/Sector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@ export class SectorShape {
startAngle = 0
endAngle = Math.PI * 2
clockwise = true
cornerRadius = 0
innerCornerRadius = 0
plainheart marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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, 10, 5, 10]
* [5, 10, 15] => [5, 10, 15, 15]
* [5, 10, 15, 20] => [5, 10, 15, 20]
*/
cornerRadius: number | string | (number | string)[] = 0
}

export interface SectorProps extends PathProps {
Expand Down
25 changes: 22 additions & 3 deletions test/sector.html
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -93,8 +95,25 @@
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, '50%'],
// clockwise: false
}
}));
</script>
Expand Down