@antv/g2
Version:
the Grammar of Graphics in Javascript
349 lines (313 loc) • 9.73 kB
text/typescript
import { vec2 } from '@antv/matrix-util';
import { each } from '@antv/util';
import { Coordinate, PathCommand } from '../../../dependents';
import { Point, Position } from '../../../interface';
import { getDistanceToCenter } from '../../../util/coordinate';
function _points2path(points: Point[], isInCircle: boolean): PathCommand[] {
const path = [];
if (points.length) {
path.push(['M', points[0].x, points[0].y]);
for (let i = 1, length = points.length; i < length; i += 1) {
const item = points[i];
path.push(['L', item.x, item.y]);
}
if (isInCircle) {
path.push(['Z']);
}
}
return path;
}
function _convertArr(arr: number[], coord: Coordinate): any[] {
const tmp = [arr[0]];
for (let i = 1, len = arr.length; i < len; i = i + 2) {
const point = coord.convert({
x: arr[i],
y: arr[i + 1],
});
tmp.push(point.x, point.y);
}
return tmp;
}
function _convertArcPath(path: PathCommand, coord: Coordinate): any[] {
const { isTransposed } = coord;
const r = path[1];
const x = path[6];
const y = path[7];
const point = coord.convert({ x, y });
const direction = isTransposed ? 0 : 1;
return ['A', r, r, 0, 0, direction, point.x, point.y];
}
function _convertPolarPath(pre: PathCommand, cur: PathCommand, coord: Coordinate): PathCommand[] {
const { isTransposed, startAngle, endAngle } = coord;
const prePoint =
pre[0].toLowerCase() === 'a'
? {
x: pre[6],
y: pre[7],
}
: {
x: pre[1],
y: pre[2],
};
const curPoint = {
x: cur[1],
y: cur[2],
};
const rst = [];
const xDim = isTransposed ? 'y' : 'x';
const angleRange = Math.abs(curPoint[xDim] - prePoint[xDim]) * (endAngle - startAngle);
const direction = curPoint[xDim] >= prePoint[xDim] ? 1 : 0; // 圆弧的方向
const flag = angleRange > Math.PI ? 1 : 0; // 大弧还是小弧标志位
const convertPoint = coord.convert(curPoint);
const r = getDistanceToCenter(coord, convertPoint);
if (r >= 0.5) {
// 小于1像素的圆在图像上无法识别
if (angleRange === Math.PI * 2) {
const middlePoint = {
x: (curPoint.x + prePoint.x) / 2,
y: (curPoint.y + prePoint.y) / 2,
};
const middleConvertPoint = coord.convert(middlePoint);
rst.push(['A', r, r, 0, flag, direction, middleConvertPoint.x, middleConvertPoint.y]);
rst.push(['A', r, r, 0, flag, direction, convertPoint.x, convertPoint.y]);
} else {
rst.push(['A', r, r, 0, flag, direction, convertPoint.x, convertPoint.y]);
}
}
return rst;
}
// 当存在整体的圆时,去除圆前面和后面的线,防止出现直线穿过整个圆的情形
function _filterFullCirleLine(path: PathCommand[]) {
each(path, (subPath, index) => {
const cur = subPath;
if (cur[0].toLowerCase() === 'a') {
const pre = path[index - 1];
const next = path[index + 1];
if (next && next[0].toLowerCase() === 'a') {
if (pre && pre[0].toLowerCase() === 'l') {
pre[0] = 'M';
}
} else if (pre && pre[0].toLowerCase() === 'a') {
if (next && next[0].toLowerCase() === 'l') {
next[0] = 'M';
}
}
}
});
}
/**
* @ignore
* 计算光滑的贝塞尔曲线
*/
export const smoothBezier = (
points: Position[],
smooth: number,
isLoop: boolean,
constraint: Position[]
): Position[] => {
const cps = [];
const hasConstraint = !!constraint;
let prevPoint: Position;
let nextPoint: Position;
let min: Position;
let max: Position;
let nextCp0: Position;
let cp1: Position;
let cp0: Position;
if (hasConstraint) {
[min, max] = constraint;
for (let i = 0, l = points.length; i < l; i++) {
const point = points[i];
min = vec2.min([0, 0], min, point) as [number, number];
max = vec2.max([0, 0], max, point) as [number, number];
}
}
for (let i = 0, len = points.length; i < len; i++) {
const point = points[i];
if (i === 0 && !isLoop) {
cp0 = point;
} else if (i === len - 1 && !isLoop) {
cp1 = point;
cps.push(cp0);
cps.push(cp1);
} else {
prevPoint = points[isLoop ? (i ? i - 1 : len - 1) : i - 1];
nextPoint = points[isLoop ? (i + 1) % len : i + 1];
let v: [number, number] = [0, 0];
v = vec2.sub(v, nextPoint, prevPoint) as [number, number];
v = vec2.scale(v, v, smooth) as [number, number];
let d0 = vec2.distance(point, prevPoint);
let d1 = vec2.distance(point, nextPoint);
const sum = d0 + d1;
if (sum !== 0) {
d0 /= sum;
d1 /= sum;
}
let v1 = vec2.scale([0, 0], v, -d0);
let v2 = vec2.scale([0, 0], v, d1);
cp1 = vec2.add([0, 0], point, v1) as Position;
nextCp0 = vec2.add([0, 0], point, v2) as Position;
// 下一个控制点必须在这个点和下一个点之间
nextCp0 = vec2.min([0, 0], nextCp0, vec2.max([0, 0], nextPoint, point)) as Position;
nextCp0 = vec2.max([0, 0], nextCp0, vec2.min([0, 0], nextPoint, point)) as Position;
// 重新计算 cp1 的值
v1 = vec2.sub([0, 0], nextCp0, point);
v1 = vec2.scale([0, 0], v1, -d0 / d1);
cp1 = vec2.add([0, 0], point, v1) as Position;
// 上一个控制点必须要在上一个点和这一个点之间
cp1 = vec2.min([0, 0], cp1, vec2.max([0, 0], prevPoint, point)) as Position;
cp1 = vec2.max([0, 0], cp1, vec2.min([0, 0], prevPoint, point)) as Position;
// 重新计算 nextCp0 的值
v2 = vec2.sub([0, 0], point, cp1);
v2 = vec2.scale([0, 0], v2, d1 / d0);
nextCp0 = vec2.add([0, 0], point, v2) as Position;
if (hasConstraint) {
cp1 = vec2.max([0, 0], cp1, min) as Position;
cp1 = vec2.min([0, 0], cp1, max) as Position;
nextCp0 = vec2.max([0, 0], nextCp0, min) as Position;
nextCp0 = vec2.min([0, 0], nextCp0, max) as Position;
}
cps.push(cp0);
cps.push(cp1);
cp0 = nextCp0;
}
}
if (isLoop) {
cps.push(cps.shift());
}
return cps;
};
/**
* @ignore
* 贝塞尔曲线
*/
export function catmullRom2bezier(crp: number[], z: boolean, constraint: Position[]): PathCommand[] {
const isLoop = !!z;
const pointList = [];
for (let i = 0, l = crp.length; i < l; i += 2) {
pointList.push([crp[i], crp[i + 1]]);
}
const controlPointList = smoothBezier(pointList, 0.4, isLoop, constraint);
const len = pointList.length;
const d1 = [];
let cp1: Position;
let cp2: Position;
let p: Position;
for (let i = 0; i < len - 1; i++) {
cp1 = controlPointList[i * 2];
cp2 = controlPointList[i * 2 + 1];
p = pointList[i + 1];
d1.push(['C', cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1]]);
}
if (isLoop) {
cp1 = controlPointList[len];
cp2 = controlPointList[len + 1];
p = pointList[0];
d1.push(['C', cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1]]);
}
return d1;
}
/**
* @ignore
* 将点连接成路径 path
*/
export function getLinePath(points: Point[], isInCircle?: boolean): PathCommand[] {
return _points2path(points, isInCircle);
}
/**
* @ignore
* 根据关键点获取限定了范围的平滑线
*/
export function getSplinePath(points: Point[], isInCircle?: boolean, constaint?: Position[]): PathCommand[] {
const data = [];
const first = points[0];
let prePoint = null;
if (points.length <= 2) {
// 两点以内直接绘制成路径
return getLinePath(points, isInCircle);
}
for (let i = 0, len = points.length; i < len; i++) {
const point = points[i];
if (!prePoint || !(prePoint.x === point.x && prePoint.y === point.y)) {
data.push(point.x);
data.push(point.y);
prePoint = point;
}
}
const constraint = constaint || [
// 范围
[0, 0],
[1, 1],
];
const splinePath = catmullRom2bezier(data, isInCircle, constraint);
splinePath.unshift(['M', first.x, first.y]);
return splinePath;
}
/**
* @ignore
* 将归一化后的路径数据转换成坐标
*/
export function convertNormalPath(coord, path: PathCommand[]): PathCommand[] {
const tmp = [];
each(path, (subPath) => {
const action = subPath[0];
switch (action.toLowerCase()) {
case 'm':
case 'l':
case 'c':
tmp.push(_convertArr(subPath, coord));
break;
case 'a':
tmp.push(_convertArcPath(subPath, coord));
break;
case 'z':
default:
tmp.push(subPath);
break;
}
});
return tmp;
}
/**
* @ignore
* 将路径转换为极坐标下的真实路径
*/
export function convertPolarPath(coord, path: PathCommand[]): PathCommand[] {
let tmp = [];
let pre: PathCommand;
let cur: PathCommand;
let transposed: boolean;
let equals: boolean;
each(path, (subPath, index) => {
const action = subPath[0];
switch (action.toLowerCase()) {
case 'm':
case 'c':
case 'q':
tmp.push(_convertArr(subPath, coord));
break;
case 'l':
pre = path[index - 1];
cur = subPath;
transposed = coord.isTransposed;
// 是否半径相同,转换成圆弧
equals = transposed ? pre[pre.length - 2] === cur[1] : pre[pre.length - 1] === cur[2];
if (equals) {
tmp = tmp.concat(_convertPolarPath(pre, cur, coord));
} else {
// y 不相等,所以直接转换
tmp.push(_convertArr(subPath, coord));
}
break;
case 'a':
tmp.push(_convertArcPath(subPath, coord));
break;
case 'z':
default:
tmp.push(subPath);
break;
}
});
_filterFullCirleLine(tmp); // 过滤多余的直线
return tmp;
}