zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
322 lines (290 loc) • 10.8 kB
text/typescript
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();
}