var Util = require('../../util/index'); var Inside = require('./inside'); var Cubic = require('../math/cubic'); var Quadratic = require('../math/quadratic'); var Ellipse = require('../math/ellipse'); var vec3 = Util.vec3; var mat3 = Util.mat3; var ARR_CMD = ['m', 'l', 'c', 'a', 'q', 'h', 'v', 't', 's', 'z']; function toAbsolute(x, y, curPoint) { // 获取绝对坐标 return { x: curPoint.x + x, y: curPoint.y + y }; } function toSymmetry(point, center) { // 点对称 return { x: center.x + (center.x - point.x), y: center.y + (center.y - point.y) }; } function vMag(v) { return Math.sqrt(v[0] * v[0] + v[1] * v[1]); } function vRatio(u, v) { return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v)); } function vAngle(u, v) { return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v)); } function getArcParams(point1, point2, fa, fs, rx, ry, psiDeg) { var psi = Util.mod(Util.toRadian(psiDeg), Math.PI * 2); var x1 = point1.x; var y1 = point1.y; var x2 = point2.x; var y2 = point2.y; var xp = Math.cos(psi) * (x1 - x2) / 2.0 + Math.sin(psi) * (y1 - y2) / 2.0; var yp = -1 * Math.sin(psi) * (x1 - x2) / 2.0 + Math.cos(psi) * (y1 - y2) / 2.0; var lambda = xp * xp / (rx * rx) + yp * yp / (ry * ry); if (lambda > 1) { rx *= Math.sqrt(lambda); ry *= Math.sqrt(lambda); } var diff = rx * rx * (yp * yp) + ry * ry * (xp * xp); var f = Math.sqrt((rx * rx * (ry * ry) - diff) / diff); if (fa === fs) { f *= -1; } if (isNaN(f)) { f = 0; } var cxp = f * rx * yp / ry; var cyp = f * -ry * xp / rx; var cx = (x1 + x2) / 2.0 + Math.cos(psi) * cxp - Math.sin(psi) * cyp; var cy = (y1 + y2) / 2.0 + Math.sin(psi) * cxp + Math.cos(psi) * cyp; var theta = vAngle([1, 0], [(xp - cxp) / rx, (yp - cyp) / ry]); var u = [(xp - cxp) / rx, (yp - cyp) / ry]; var v = [(-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry]; var dTheta = vAngle(u, v); if (vRatio(u, v) <= -1) { dTheta = Math.PI; } if (vRatio(u, v) >= 1) { dTheta = 0; } if (fs === 0 && dTheta > 0) { dTheta = dTheta - 2 * Math.PI; } if (fs === 1 && dTheta < 0) { dTheta = dTheta + 2 * Math.PI; } return [point1, cx, cy, rx, ry, theta, dTheta, psi, fs]; } var PathSegment = function PathSegment(item, preSegment, isLast) { this.preSegment = preSegment; this.isLast = isLast; this.init(item, preSegment); }; Util.augment(PathSegment, { init: function init(item, preSegment) { var command = item[0]; preSegment = preSegment || { endPoint: { x: 0, y: 0 } }; var relative = ARR_CMD.indexOf(command) >= 0; // /[a-z]/.test(command); var cmd = relative ? command.toUpperCase() : command; var p = item; var point1; var point2; var point3; var point; var preEndPoint = preSegment.endPoint; var p1 = p[1]; var p2 = p[2]; switch (cmd) { default: break; case 'M': if (relative) { point = toAbsolute(p1, p2, preEndPoint); } else { point = { x: p1, y: p2 }; } this.command = 'M'; this.params = [preEndPoint, point]; this.subStart = point; this.endPoint = point; break; case 'L': if (relative) { point = toAbsolute(p1, p2, preEndPoint); } else { point = { x: p1, y: p2 }; } this.command = 'L'; this.params = [preEndPoint, point]; this.subStart = preSegment.subStart; this.endPoint = point; this.endTangent = function () { return [point.x - preEndPoint.x, point.y - preEndPoint.y]; }; this.startTangent = function () { return [preEndPoint.x - point.x, preEndPoint.y - point.y]; }; break; case 'H': if (relative) { point = toAbsolute(p1, 0, preEndPoint); } else { point = { x: p1, y: preEndPoint.y }; } this.command = 'L'; this.params = [preEndPoint, point]; this.subStart = preSegment.subStart; this.endPoint = point; this.endTangent = function () { return [point.x - preEndPoint.x, point.y - preEndPoint.y]; }; this.startTangent = function () { return [preEndPoint.x - point.x, preEndPoint.y - point.y]; }; break; case 'V': if (relative) { point = toAbsolute(0, p1, preEndPoint); } else { point = { x: preEndPoint.x, y: p1 }; } this.command = 'L'; this.params = [preEndPoint, point]; this.subStart = preSegment.subStart; this.endPoint = point; this.endTangent = function () { return [point.x - preEndPoint.x, point.y - preEndPoint.y]; }; this.startTangent = function () { return [preEndPoint.x - point.x, preEndPoint.y - point.y]; }; break; case 'Q': if (relative) { point1 = toAbsolute(p1, p2, preEndPoint); point2 = toAbsolute(p[3], p[4], preEndPoint); } else { point1 = { x: p1, y: p2 }; point2 = { x: p[3], y: p[4] }; } this.command = 'Q'; this.params = [preEndPoint, point1, point2]; this.subStart = preSegment.subStart; this.endPoint = point2; this.endTangent = function () { return [point2.x - point1.x, point2.y - point1.y]; }; this.startTangent = function () { return [preEndPoint.x - point1.x, preEndPoint.y - point1.y]; }; break; case 'T': if (relative) { point2 = toAbsolute(p1, p2, preEndPoint); } else { point2 = { x: p1, y: p2 }; } if (preSegment.command === 'Q') { point1 = toSymmetry(preSegment.params[1], preEndPoint); this.command = 'Q'; this.params = [preEndPoint, point1, point2]; this.subStart = preSegment.subStart; this.endPoint = point2; this.endTangent = function () { return [point2.x - point1.x, point2.y - point1.y]; }; this.startTangent = function () { return [preEndPoint.x - point1.x, preEndPoint.y - point1.y]; }; } else { this.command = 'TL'; this.params = [preEndPoint, point2]; this.subStart = preSegment.subStart; this.endPoint = point2; this.endTangent = function () { return [point2.x - preEndPoint.x, point2.y - preEndPoint.y]; }; this.startTangent = function () { return [preEndPoint.x - point2.x, preEndPoint.y - point2.y]; }; } break; case 'C': if (relative) { point1 = toAbsolute(p1, p2, preEndPoint); point2 = toAbsolute(p[3], p[4], preEndPoint); point3 = toAbsolute(p[5], p[6], preEndPoint); } else { point1 = { x: p1, y: p2 }; point2 = { x: p[3], y: p[4] }; point3 = { x: p[5], y: p[6] }; } this.command = 'C'; this.params = [preEndPoint, point1, point2, point3]; this.subStart = preSegment.subStart; this.endPoint = point3; this.endTangent = function () { return [point3.x - point2.x, point3.y - point2.y]; }; this.startTangent = function () { return [preEndPoint.x - point1.x, preEndPoint.y - point1.y]; }; break; case 'S': if (relative) { point2 = toAbsolute(p1, p2, preEndPoint); point3 = toAbsolute(p[3], p[4], preEndPoint); } else { point2 = { x: p1, y: p2 }; point3 = { x: p[3], y: p[4] }; } if (preSegment.command === 'C') { point1 = toSymmetry(preSegment.params[2], preEndPoint); this.command = 'C'; this.params = [preEndPoint, point1, point2, point3]; this.subStart = preSegment.subStart; this.endPoint = point3; this.endTangent = function () { return [point3.x - point2.x, point3.y - point2.y]; }; this.startTangent = function () { return [preEndPoint.x - point1.x, preEndPoint.y - point1.y]; }; } else { this.command = 'SQ'; this.params = [preEndPoint, point2, point3]; this.subStart = preSegment.subStart; this.endPoint = point3; this.endTangent = function () { return [point3.x - point2.x, point3.y - point2.y]; }; this.startTangent = function () { return [preEndPoint.x - point2.x, preEndPoint.y - point2.y]; }; } break; case 'A': { var rx = p1; var ry = p2; var psi = p[3]; var fa = p[4]; var fs = p[5]; if (relative) { point = toAbsolute(p[6], p[7], preEndPoint); } else { point = { x: p[6], y: p[7] }; } this.command = 'A'; var params = getArcParams(preEndPoint, point, fa, fs, rx, ry, psi); this.params = params; var start = preSegment.subStart; this.subStart = start; this.endPoint = point; var startAngle = params[5] % (Math.PI * 2); if (Util.isNumberEqual(startAngle, Math.PI * 2)) { startAngle = 0; } var endAngle = params[6] % (Math.PI * 2); if (Util.isNumberEqual(endAngle, Math.PI * 2)) { endAngle = 0; } var d = 0.001; this.startTangent = function () { if (fs === 0) { d *= -1; } var dx = params[3] * Math.cos(startAngle - d) + params[1]; var dy = params[4] * Math.sin(startAngle - d) + params[2]; return [dx - start.x, dy - start.y]; }; this.endTangent = function () { var endAngle = params[6]; if (endAngle - Math.PI * 2 < 0.0001) { endAngle = 0; } var dx = params[3] * Math.cos(startAngle + endAngle + d) + params[1]; var dy = params[4] * Math.sin(startAngle + endAngle - d) + params[2]; return [preEndPoint.x - dx, preEndPoint.y - dy]; }; break; } case 'Z': { this.command = 'Z'; this.params = [preEndPoint, preSegment.subStart]; this.subStart = preSegment.subStart; this.endPoint = preSegment.subStart; } } }, isInside: function isInside(x, y, lineWidth) { var self = this; var command = self.command; var params = self.params; var box = self.box; if (box) { if (!Inside.box(box.minX, box.maxX, box.minY, box.maxY, x, y)) { return false; } } switch (command) { default: break; case 'M': return false; case 'TL': case 'L': case 'Z': return Inside.line(params[0].x, params[0].y, params[1].x, params[1].y, lineWidth, x, y); case 'SQ': case 'Q': return Inside.quadraticline(params[0].x, params[0].y, params[1].x, params[1].y, params[2].x, params[2].y, lineWidth, x, y); case 'C': { return Inside.cubicline(params[0].x, params[0].y, params[1].x, params[1].y, params[2].x, params[2].y, params[3].x, params[3].y, lineWidth, x, y); } case 'A': { var p = params; var cx = p[1]; var cy = p[2]; var rx = p[3]; var ry = p[4]; var theta = p[5]; var dTheta = p[6]; var psi = p[7]; var fs = p[8]; var r = rx > ry ? rx : ry; var scaleX = rx > ry ? 1 : rx / ry; var scaleY = rx > ry ? ry / rx : 1; p = [x, y, 1]; var m = [1, 0, 0, 0, 1, 0, 0, 0, 1]; mat3.translate(m, m, [-cx, -cy]); mat3.rotate(m, m, -psi); mat3.scale(m, m, [1 / scaleX, 1 / scaleY]); vec3.transformMat3(p, p, m); return Inside.arcline(0, 0, r, theta, theta + dTheta, 1 - fs, lineWidth, p[0], p[1]); } } return false; }, draw: function draw(context) { var command = this.command; var params = this.params; var point1; var point2; var point3; switch (command) { default: break; case 'M': context.moveTo(params[1].x, params[1].y); break; case 'TL': case 'L': context.lineTo(params[1].x, params[1].y); break; case 'SQ': case 'Q': point1 = params[1]; point2 = params[2]; context.quadraticCurveTo(point1.x, point1.y, point2.x, point2.y); break; case 'C': point1 = params[1]; point2 = params[2]; point3 = params[3]; context.bezierCurveTo(point1.x, point1.y, point2.x, point2.y, point3.x, point3.y); break; case 'A': { var p = params; var p1 = p[1]; var p2 = p[2]; var cx = p1; var cy = p2; var rx = p[3]; var ry = p[4]; var theta = p[5]; var dTheta = p[6]; var psi = p[7]; var fs = p[8]; var r = rx > ry ? rx : ry; var scaleX = rx > ry ? 1 : rx / ry; var scaleY = rx > ry ? ry / rx : 1; context.translate(cx, cy); context.rotate(psi); context.scale(scaleX, scaleY); context.arc(0, 0, r, theta, theta + dTheta, 1 - fs); context.scale(1 / scaleX, 1 / scaleY); context.rotate(-psi); context.translate(-cx, -cy); break; } case 'Z': context.closePath(); break; } }, shortenDraw: function shortenDraw(context, dx, dy) { var command = this.command; var params = this.params; var point1; var point2; var point3; switch (command) { default: break; case 'M': context.moveTo(params[1].x - dx, params[1].y - dy); break; case 'TL': case 'L': context.lineTo(params[1].x - dx, params[1].y - dy); break; case 'SQ': case 'Q': point1 = params[1]; point2 = params[2]; context.quadraticCurveTo(point1.x, point1.y, point2.x - dx, point2.y - dy); break; case 'C': point1 = params[1]; point2 = params[2]; point3 = params[3]; context.bezierCurveTo(point1.x, point1.y, point2.x, point2.y, point3.x - dx, point3.y - dy); break; case 'A': { var p = params; var p1 = p[1]; var p2 = p[2]; var cx = p1; var cy = p2; var rx = p[3]; var ry = p[4]; var theta = p[5]; var dTheta = p[6]; var psi = p[7]; var fs = p[8]; var r = rx > ry ? rx : ry; var scaleX = rx > ry ? 1 : rx / ry; var scaleY = rx > ry ? ry / rx : 1; context.translate(cx, cy); context.rotate(psi); context.scale(scaleX, scaleY); context.arc(0, 0, r, theta, theta + dTheta, 1 - fs); context.scale(1 / scaleX, 1 / scaleY); context.rotate(-psi); context.translate(-cx, -cy); break; } case 'Z': context.closePath(); break; } }, getBBox: function getBBox(lineWidth) { var halfWidth = lineWidth / 2; var params = this.params; var yDims; var xDims; var i; var l; switch (this.command) { default: case 'M': case 'Z': break; case 'TL': case 'L': this.box = { minX: Math.min(params[0].x, params[1].x) - halfWidth, maxX: Math.max(params[0].x, params[1].x) + halfWidth, minY: Math.min(params[0].y, params[1].y) - halfWidth, maxY: Math.max(params[0].y, params[1].y) + halfWidth }; break; case 'SQ': case 'Q': xDims = Quadratic.extrema(params[0].x, params[1].x, params[2].x); for (i = 0, l = xDims.length; i < l; i++) { xDims[i] = Quadratic.at(params[0].x, params[1].x, params[2].x, xDims[i]); } xDims.push(params[0].x, params[2].x); yDims = Quadratic.extrema(params[0].y, params[1].y, params[2].y); for (i = 0, l = yDims.length; i < l; i++) { yDims[i] = Quadratic.at(params[0].y, params[1].y, params[2].y, yDims); } yDims.push(params[0].y, params[2].y); this.box = { minX: Math.min.apply(Math, xDims) - halfWidth, maxX: Math.max.apply(Math, xDims) + halfWidth, minY: Math.min.apply(Math, yDims) - halfWidth, maxY: Math.max.apply(Math, yDims) + halfWidth }; break; case 'C': xDims = Cubic.extrema(params[0].x, params[1].x, params[2].x, params[3].x); for (i = 0, l = xDims.length; i < l; i++) { xDims[i] = Cubic.at(params[0].x, params[1].x, params[2].x, params[3].x, xDims[i]); } yDims = Cubic.extrema(params[0].y, params[1].y, params[2].y, params[3].y); for (i = 0, l = yDims.length; i < l; i++) { yDims[i] = Cubic.at(params[0].y, params[1].y, params[2].y, params[3].y, yDims[i]); } xDims.push(params[0].x, params[3].x); yDims.push(params[0].y, params[3].y); this.box = { minX: Math.min.apply(Math, xDims) - halfWidth, maxX: Math.max.apply(Math, xDims) + halfWidth, minY: Math.min.apply(Math, yDims) - halfWidth, maxY: Math.max.apply(Math, yDims) + halfWidth }; break; case 'A': { // todo 待优化 var p = params; var cx = p[1]; var cy = p[2]; var rx = p[3]; var ry = p[4]; var theta = p[5]; var dTheta = p[6]; var psi = p[7]; var fs = p[8]; var start = theta; var end = theta + dTheta; var xDim = Ellipse.xExtrema(psi, rx, ry); var minX = Infinity; var maxX = -Infinity; var xs = [start, end]; for (i = -Math.PI * 2; i <= Math.PI * 2; i += Math.PI) { var xAngle = xDim + i; if (fs === 1) { if (start < xAngle && xAngle < end) { xs.push(xAngle); } } else { if (end < xAngle && xAngle < start) { xs.push(xAngle); } } } for (i = 0, l = xs.length; i < l; i++) { var x = Ellipse.xAt(psi, rx, ry, cx, xs[i]); if (x < minX) { minX = x; } if (x > maxX) { maxX = x; } } var yDim = Ellipse.yExtrema(psi, rx, ry); var minY = Infinity; var maxY = -Infinity; var ys = [start, end]; for (i = -Math.PI * 2; i <= Math.PI * 2; i += Math.PI) { var yAngle = yDim + i; if (fs === 1) { if (start < yAngle && yAngle < end) { ys.push(yAngle); } } else { if (end < yAngle && yAngle < start) { ys.push(yAngle); } } } for (i = 0, l = ys.length; i < l; i++) { var y = Ellipse.yAt(psi, rx, ry, cy, ys[i]); if (y < minY) { minY = y; } if (y > maxY) { maxY = y; } } this.box = { minX: minX - halfWidth, maxX: maxX + halfWidth, minY: minY - halfWidth, maxY: maxY + halfWidth }; break; } } } }); module.exports = PathSegment;