package.src.tool.convertPath.ts Maven / Gradle / Ivy
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;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy