import { cubicSubdivide } from '../core/curve';
|
import PathProxy from '../core/PathProxy';
|
|
const CMD = PathProxy.CMD;
|
|
function aroundEqual(a: number, b: number) {
|
return Math.abs(a - b) < 1e-5;
|
}
|
|
export function pathToBezierCurves(path: PathProxy) {
|
|
const data = path.data;
|
const len = path.len();
|
|
const bezierArrayGroups: number[][] = [];
|
let currentSubpath: number[];
|
|
let xi = 0;
|
let yi = 0;
|
let x0 = 0;
|
let y0 = 0;
|
|
function createNewSubpath(x: number, y: number) {
|
// More than one M command
|
if (currentSubpath && currentSubpath.length > 2) {
|
bezierArrayGroups.push(currentSubpath);
|
}
|
currentSubpath = [x, y];
|
}
|
|
function addLine(x0: number, y0: number, x1: number, y1: number) {
|
if (!(aroundEqual(x0, x1) && aroundEqual(y0, y1))) {
|
currentSubpath.push(x0, y0, x1, y1, x1, y1);
|
}
|
}
|
|
function addArc(startAngle: number, endAngle: number, cx: number, cy: number, rx: number, ry: number) {
|
// https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves
|
const delta = Math.abs(endAngle - startAngle);
|
const len = Math.tan(delta / 4) * 4 / 3;
|
const dir = endAngle < startAngle ? -1 : 1;
|
|
const c1 = Math.cos(startAngle);
|
const s1 = Math.sin(startAngle);
|
const c2 = Math.cos(endAngle);
|
const s2 = Math.sin(endAngle);
|
|
const x1 = c1 * rx + cx;
|
const y1 = s1 * ry + cy;
|
|
const x4 = c2 * rx + cx;
|
const y4 = s2 * ry + cy;
|
|
const hx = rx * len * dir;
|
const hy = ry * len * dir;
|
currentSubpath.push(
|
// Move control points on tangent.
|
x1 - hx * s1, y1 + hy * c1,
|
x4 + hx * s2, y4 - hy * c2,
|
x4, y4
|
);
|
}
|
|
let x1;
|
let y1;
|
let x2;
|
let y2;
|
|
for (let i = 0; i < len;) {
|
const cmd = data[i++];
|
const isFirst = i === 1;
|
|
if (isFirst) {
|
// 如果第一个命令是 L, C, Q
|
// 则 previous point 同绘制命令的第一个 point
|
// 第一个命令为 Arc 的情况下会在后面特殊处理
|
xi = data[i];
|
yi = data[i + 1];
|
|
x0 = xi;
|
y0 = yi;
|
|
if (cmd === CMD.L || cmd === CMD.C || cmd === CMD.Q) {
|
// Start point
|
currentSubpath = [x0, y0];
|
}
|
}
|
|
switch (cmd) {
|
case CMD.M:
|
// moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
|
// 在 closePath 的时候使用
|
xi = x0 = data[i++];
|
yi = y0 = data[i++];
|
|
createNewSubpath(x0, y0);
|
break;
|
case CMD.L:
|
x1 = data[i++];
|
y1 = data[i++];
|
addLine(xi, yi, x1, y1);
|
xi = x1;
|
yi = y1;
|
break;
|
case CMD.C:
|
currentSubpath.push(
|
data[i++], data[i++], data[i++], data[i++],
|
xi = data[i++], yi = data[i++]
|
);
|
break;
|
case CMD.Q:
|
x1 = data[i++];
|
y1 = data[i++];
|
x2 = data[i++];
|
y2 = data[i++];
|
currentSubpath.push(
|
// Convert quadratic to cubic
|
xi + 2 / 3 * (x1 - xi), yi + 2 / 3 * (y1 - yi),
|
x2 + 2 / 3 * (x1 - x2), y2 + 2 / 3 * (y1 - y2),
|
x2, y2
|
);
|
xi = x2;
|
yi = y2;
|
break;
|
case CMD.A:
|
const cx = data[i++];
|
const cy = data[i++];
|
const rx = data[i++];
|
const ry = data[i++];
|
const startAngle = data[i++];
|
const endAngle = data[i++] + startAngle;
|
|
// TODO Arc rotation
|
i += 1;
|
const anticlockwise = !data[i++];
|
|
x1 = Math.cos(startAngle) * rx + cx;
|
y1 = Math.sin(startAngle) * ry + cy;
|
if (isFirst) {
|
// 直接使用 arc 命令
|
// 第一个命令起点还未定义
|
x0 = x1;
|
y0 = y1;
|
createNewSubpath(x0, y0);
|
}
|
else {
|
// Connect a line between current point to arc start point.
|
addLine(xi, yi, x1, y1);
|
}
|
|
xi = Math.cos(endAngle) * rx + cx;
|
yi = Math.sin(endAngle) * ry + cy;
|
|
const step = (anticlockwise ? -1 : 1) * Math.PI / 2;
|
|
for (let angle = startAngle; anticlockwise ? angle > endAngle : angle < endAngle; angle += step) {
|
const nextAngle = anticlockwise ? Math.max(angle + step, endAngle)
|
: Math.min(angle + step, endAngle);
|
addArc(angle, nextAngle, cx, cy, rx, ry);
|
}
|
|
break;
|
case CMD.R:
|
x0 = xi = data[i++];
|
y0 = yi = data[i++];
|
x1 = x0 + data[i++];
|
y1 = y0 + data[i++];
|
|
// rect is an individual path.
|
createNewSubpath(x1, y0);
|
addLine(x1, y0, x1, y1);
|
addLine(x1, y1, x0, y1);
|
addLine(x0, y1, x0, y0);
|
addLine(x0, y0, x1, y0);
|
break;
|
case CMD.Z:
|
currentSubpath && addLine(xi, yi, x0, y0);
|
xi = x0;
|
yi = y0;
|
break;
|
}
|
}
|
|
if (currentSubpath && currentSubpath.length > 2) {
|
bezierArrayGroups.push(currentSubpath);
|
}
|
|
return bezierArrayGroups;
|
}
|
|
function adpativeBezier(
|
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number,
|
out: number[], scale: number
|
) {
|
// This bezier is used to simulates a line when converting path to beziers.
|
if (aroundEqual(x0, x1) && aroundEqual(y0, y1) && aroundEqual(x2, x3) && aroundEqual(y2, y3)) {
|
out.push(x3, y3);
|
return;
|
}
|
|
const PIXEL_DISTANCE = 2 / scale;
|
const PIXEL_DISTANCE_SQR = PIXEL_DISTANCE * PIXEL_DISTANCE;
|
|
// Determine if curve is straight enough
|
let dx = x3 - x0;
|
let dy = y3 - y0;
|
const d = Math.sqrt(dx * dx + dy * dy);
|
dx /= d;
|
dy /= d;
|
|
const dx1 = x1 - x0;
|
const dy1 = y1 - y0;
|
const dx2 = x2 - x3;
|
const dy2 = y2 - y3;
|
|
const cp1LenSqr = dx1 * dx1 + dy1 * dy1;
|
const cp2LenSqr = dx2 * dx2 + dy2 * dy2;
|
|
if (cp1LenSqr < PIXEL_DISTANCE_SQR && cp2LenSqr < PIXEL_DISTANCE_SQR) {
|
// Add small segment
|
out.push(x3, y3);
|
return;
|
}
|
|
// Project length of cp1
|
const projLen1 = dx * dx1 + dy * dy1;
|
// Project length of cp2
|
const projLen2 = -dx * dx2 - dy * dy2;
|
|
// Distance from cp1 to start-end line.
|
const d1Sqr = cp1LenSqr - projLen1 * projLen1;
|
// Distance from cp2 to start-end line.
|
const d2Sqr = cp2LenSqr - projLen2 * projLen2;
|
|
// IF the cp1 and cp2 is near to the start-line enough
|
// We treat it straight enough
|
if (d1Sqr < PIXEL_DISTANCE_SQR && projLen1 >= 0
|
&& d2Sqr < PIXEL_DISTANCE_SQR && projLen2 >= 0
|
) {
|
out.push(x3, y3);
|
return;
|
}
|
|
|
const tmpSegX: number[] = [];
|
const tmpSegY: number[] = [];
|
// Subdivide
|
cubicSubdivide(x0, x1, x2, x3, 0.5, tmpSegX);
|
cubicSubdivide(y0, y1, y2, y3, 0.5, tmpSegY);
|
|
adpativeBezier(
|
tmpSegX[0], tmpSegY[0], tmpSegX[1], tmpSegY[1], tmpSegX[2], tmpSegY[2], tmpSegX[3], tmpSegY[3],
|
out, scale
|
);
|
adpativeBezier(
|
tmpSegX[4], tmpSegY[4], tmpSegX[5], tmpSegY[5], tmpSegX[6], tmpSegY[6], tmpSegX[7], tmpSegY[7],
|
out, scale
|
);
|
}
|
|
export function pathToPolygons(path: PathProxy, scale?: number) {
|
// TODO Optimize simple case like path is polygon and rect?
|
const bezierArrayGroups = pathToBezierCurves(path);
|
|
const polygons: number[][] = [];
|
|
scale = scale || 1;
|
|
for (let i = 0; i < bezierArrayGroups.length; i++) {
|
const beziers = bezierArrayGroups[i];
|
const polygon: number[] = [];
|
let x0 = beziers[0];
|
let y0 = beziers[1];
|
|
polygon.push(x0, y0);
|
|
for (let k = 2; k < beziers.length;) {
|
|
const x1 = beziers[k++];
|
const y1 = beziers[k++];
|
const x2 = beziers[k++];
|
const y2 = beziers[k++];
|
const x3 = beziers[k++];
|
const y3 = beziers[k++];
|
|
adpativeBezier(x0, y0, x1, y1, x2, y2, x3, y3, polygon, scale);
|
|
x0 = x3;
|
y0 = y3;
|
}
|
|
polygons.push(polygon);
|
}
|
return polygons;
|
}
|