zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
409 lines (387 loc) • 12.8 kB
text/typescript
import PathProxy from '../core/PathProxy';
import * as line from './line';
import * as cubic from './cubic';
import * as quadratic from './quadratic';
import * as arc from './arc';
import * as curve from '../core/curve';
import windingLine from './windingLine';
const CMD = PathProxy.CMD;
const PI2 = Math.PI * 2;
const EPSILON = 1e-4;
function isAroundEqual(a: number, b: number) {
return Math.abs(a - b) < EPSILON;
}
// 临时数组
const roots = [-1, -1, -1];
const extrema = [-1, -1];
function swapExtrema() {
const tmp = extrema[0];
extrema[0] = extrema[1];
extrema[1] = tmp;
}
function windingCubic(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number,
x: number, y: number
): number {
// Quick reject
if (
(y > y0 && y > y1 && y > y2 && y > y3)
|| (y < y0 && y < y1 && y < y2 && y < y3)
) {
return 0;
}
const nRoots = curve.cubicRootAt(y0, y1, y2, y3, y, roots);
if (nRoots === 0) {
return 0;
}
else {
let w = 0;
let nExtrema = -1;
let y0_;
let y1_;
for (let i = 0; i < nRoots; i++) {
let t = roots[i];
// Avoid winding error when intersection point is the connect point of two line of polygon
let unit = (t === 0 || t === 1) ? 0.5 : 1;
let x_ = curve.cubicAt(x0, x1, x2, x3, t);
if (x_ < x) { // Quick reject
continue;
}
if (nExtrema < 0) {
nExtrema = curve.cubicExtrema(y0, y1, y2, y3, extrema);
if (extrema[1] < extrema[0] && nExtrema > 1) {
swapExtrema();
}
y0_ = curve.cubicAt(y0, y1, y2, y3, extrema[0]);
if (nExtrema > 1) {
y1_ = curve.cubicAt(y0, y1, y2, y3, extrema[1]);
}
}
if (nExtrema === 2) {
// 分成三段单调函数
if (t < extrema[0]) {
w += y0_ < y0 ? unit : -unit;
}
else if (t < extrema[1]) {
w += y1_ < y0_ ? unit : -unit;
}
else {
w += y3 < y1_ ? unit : -unit;
}
}
else {
// 分成两段单调函数
if (t < extrema[0]) {
w += y0_ < y0 ? unit : -unit;
}
else {
w += y3 < y0_ ? unit : -unit;
}
}
}
return w;
}
}
function windingQuadratic(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number,
x: number, y: number
): number {
// Quick reject
if (
(y > y0 && y > y1 && y > y2)
|| (y < y0 && y < y1 && y < y2)
) {
return 0;
}
const nRoots = curve.quadraticRootAt(y0, y1, y2, y, roots);
if (nRoots === 0) {
return 0;
}
else {
const t = curve.quadraticExtremum(y0, y1, y2);
if (t >= 0 && t <= 1) {
let w = 0;
let y_ = curve.quadraticAt(y0, y1, y2, t);
for (let i = 0; i < nRoots; i++) {
// Remove one endpoint.
let unit = (roots[i] === 0 || roots[i] === 1) ? 0.5 : 1;
let x_ = curve.quadraticAt(x0, x1, x2, roots[i]);
if (x_ < x) { // Quick reject
continue;
}
if (roots[i] < t) {
w += y_ < y0 ? unit : -unit;
}
else {
w += y2 < y_ ? unit : -unit;
}
}
return w;
}
else {
// Remove one endpoint.
const unit = (roots[0] === 0 || roots[0] === 1) ? 0.5 : 1;
const x_ = curve.quadraticAt(x0, x1, x2, roots[0]);
if (x_ < x) { // Quick reject
return 0;
}
return y2 < y0 ? unit : -unit;
}
}
}
// TODO
// Arc 旋转
// startAngle, endAngle has been normalized by normalizeArcAngles
function windingArc(
cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean,
x: number, y: number
) {
y -= cy;
if (y > r || y < -r) {
return 0;
}
const tmp = Math.sqrt(r * r - y * y);
roots[0] = -tmp;
roots[1] = tmp;
const dTheta = Math.abs(startAngle - endAngle);
if (dTheta < 1e-4) {
return 0;
}
if (dTheta >= PI2 - 1e-4) {
// Is a circle
startAngle = 0;
endAngle = PI2;
const dir = anticlockwise ? 1 : -1;
if (x >= roots[0] + cx && x <= roots[1] + cx) {
return dir;
}
else {
return 0;
}
}
if (startAngle > endAngle) {
// Swap, make sure startAngle is smaller than endAngle.
const tmp = startAngle;
startAngle = endAngle;
endAngle = tmp;
}
// endAngle - startAngle is normalized to 0 - 2*PI.
// So following will normalize them to 0 - 4*PI
if (startAngle < 0) {
startAngle += PI2;
endAngle += PI2;
}
let w = 0;
for (let i = 0; i < 2; i++) {
const x_ = roots[i];
if (x_ + cx > x) {
let angle = Math.atan2(y, x_);
let dir = anticlockwise ? 1 : -1;
if (angle < 0) {
angle = PI2 + angle;
}
if (
(angle >= startAngle && angle <= endAngle)
|| (angle + PI2 >= startAngle && angle + PI2 <= endAngle)
) {
if (angle > Math.PI / 2 && angle < Math.PI * 1.5) {
dir = -dir;
}
w += dir;
}
}
}
return w;
}
function containPath(
path: PathProxy, lineWidth: number, isStroke: boolean, x: number, y: number
): boolean {
const data = path.data;
const len = path.len();
let w = 0;
let xi = 0;
let yi = 0;
let x0 = 0;
let y0 = 0;
let x1;
let y1;
for (let i = 0; i < len;) {
const cmd = data[i++];
const isFirst = i === 1;
// Begin a new subpath
if (cmd === CMD.M && i > 1) {
// Close previous subpath
if (!isStroke) {
w += windingLine(xi, yi, x0, y0, x, y);
}
// 如果被任何一个 subpath 包含
// if (w !== 0) {
// return true;
// }
}
if (isFirst) {
// 如果第一个命令是 L, C, Q
// 则 previous point 同绘制命令的第一个 point
//
// 第一个命令为 Arc 的情况下会在后面特殊处理
xi = data[i];
yi = data[i + 1];
x0 = xi;
y0 = yi;
}
switch (cmd) {
case CMD.M:
// moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
// 在 closePath 的时候使用
x0 = data[i++];
y0 = data[i++];
xi = x0;
yi = y0;
break;
case CMD.L:
if (isStroke) {
if (line.containStroke(xi, yi, data[i], data[i + 1], lineWidth, x, y)) {
return true;
}
}
else {
// NOTE 在第一个命令为 L, C, Q 的时候会计算出 NaN
w += windingLine(xi, yi, data[i], data[i + 1], x, y) || 0;
}
xi = data[i++];
yi = data[i++];
break;
case CMD.C:
if (isStroke) {
if (cubic.containStroke(xi, yi,
data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1],
lineWidth, x, y
)) {
return true;
}
}
else {
w += windingCubic(
xi, yi,
data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1],
x, y
) || 0;
}
xi = data[i++];
yi = data[i++];
break;
case CMD.Q:
if (isStroke) {
if (quadratic.containStroke(xi, yi,
data[i++], data[i++], data[i], data[i + 1],
lineWidth, x, y
)) {
return true;
}
}
else {
w += windingQuadratic(
xi, yi,
data[i++], data[i++], data[i], data[i + 1],
x, y
) || 0;
}
xi = data[i++];
yi = data[i++];
break;
case CMD.A:
// TODO Arc 判断的开销比较大
const cx = data[i++];
const cy = data[i++];
const rx = data[i++];
const ry = data[i++];
const theta = data[i++];
const dTheta = data[i++];
// TODO Arc 旋转
i += 1;
const anticlockwise = !!(1 - data[i++]);
x1 = Math.cos(theta) * rx + cx;
y1 = Math.sin(theta) * ry + cy;
// 不是直接使用 arc 命令
if (!isFirst) {
w += windingLine(xi, yi, x1, y1, x, y);
}
else {
// 第一个命令起点还未定义
x0 = x1;
y0 = y1;
}
// zr 使用scale来模拟椭圆, 这里也对x做一定的缩放
const _x = (x - cx) * ry / rx + cx;
if (isStroke) {
if (arc.containStroke(
cx, cy, ry, theta, theta + dTheta, anticlockwise,
lineWidth, _x, y
)) {
return true;
}
}
else {
w += windingArc(
cx, cy, ry, theta, theta + dTheta, anticlockwise,
_x, y
);
}
xi = Math.cos(theta + dTheta) * rx + cx;
yi = Math.sin(theta + dTheta) * ry + cy;
break;
case CMD.R:
x0 = xi = data[i++];
y0 = yi = data[i++];
const width = data[i++];
const height = data[i++];
x1 = x0 + width;
y1 = y0 + height;
if (isStroke) {
if (line.containStroke(x0, y0, x1, y0, lineWidth, x, y)
|| line.containStroke(x1, y0, x1, y1, lineWidth, x, y)
|| line.containStroke(x1, y1, x0, y1, lineWidth, x, y)
|| line.containStroke(x0, y1, x0, y0, lineWidth, x, y)
) {
return true;
}
}
else {
// FIXME Clockwise ?
w += windingLine(x1, y0, x1, y1, x, y);
w += windingLine(x0, y1, x0, y0, x, y);
}
break;
case CMD.Z:
if (isStroke) {
if (line.containStroke(
xi, yi, x0, y0, lineWidth, x, y
)) {
return true;
}
}
else {
// Close a subpath
w += windingLine(xi, yi, x0, y0, x, y);
// 如果被任何一个 subpath 包含
// FIXME subpaths may overlap
// if (w !== 0) {
// return true;
// }
}
xi = x0;
yi = y0;
break;
}
}
if (!isStroke && !isAroundEqual(yi, y0)) {
w += windingLine(xi, yi, x0, y0, x, y) || 0;
}
return w !== 0;
}
export function contain(pathProxy: PathProxy, x: number, y: number): boolean {
return containPath(pathProxy, 0, false, x, y);
}
export function containStroke(pathProxy: PathProxy, lineWidth: number, x: number, y: number): boolean {
return containPath(pathProxy, lineWidth, true, x, y);
}