1177 lines
38 KiB
JavaScript
1177 lines
38 KiB
JavaScript
/**
|
|
* Class representing a path.
|
|
* Designed to be compatible with [CanvasPathMethods](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#canvaspathmethods)
|
|
* and will hopefully be replaced by the browsers' implementation of the Path object.
|
|
*/
|
|
Ext.define("Ext.draw.Path", {
|
|
requires: ['Ext.draw.Draw', 'Ext.draw.Solver'],
|
|
statics: {
|
|
pathRe: /,?([achlmqrstvxz]),?/gi,
|
|
pathRe2: /-/gi,
|
|
pathSplitRe: /\s|,/g
|
|
},
|
|
svgString: '',
|
|
|
|
/**
|
|
* Create a path from pathString
|
|
* @constructor
|
|
* @param pathString
|
|
*/
|
|
constructor: function (pathString) {
|
|
var me = this;
|
|
me.coords = [];
|
|
me.types = [];
|
|
me.cursor = null;
|
|
me.startX = 0;
|
|
me.startY = 0;
|
|
me.solvers = {};
|
|
if (pathString) {
|
|
me.fromSvgString(pathString);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear the path.
|
|
*/
|
|
clear: function () {
|
|
var me = this;
|
|
me.coords.length = 0;
|
|
me.types.length = 0;
|
|
me.cursor = null;
|
|
me.startX = 0;
|
|
me.startY = 0;
|
|
me.solvers = {};
|
|
me.dirt();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
dirt: function () {
|
|
this.svgString = '';
|
|
},
|
|
|
|
/**
|
|
* Move to a position.
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
moveTo: function (x, y) {
|
|
var me = this;
|
|
if (!me.cursor) {
|
|
me.cursor = [x, y];
|
|
}
|
|
me.coords.push(x, y);
|
|
me.types.push('M');
|
|
me.startX = x;
|
|
me.startY = y;
|
|
me.cursor[0] = x;
|
|
me.cursor[1] = y;
|
|
me.dirt();
|
|
},
|
|
|
|
/**
|
|
* A straight line to a position.
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
lineTo: function (x, y) {
|
|
var me = this;
|
|
if (!me.cursor) {
|
|
me.cursor = [x, y];
|
|
me.coords.push(x, y);
|
|
me.types.push('M');
|
|
} else {
|
|
me.coords.push(x, y);
|
|
me.types.push('L');
|
|
}
|
|
me.cursor[0] = x;
|
|
me.cursor[1] = y;
|
|
me.dirt();
|
|
},
|
|
|
|
/**
|
|
* A cubic bezier curve to a position.
|
|
* @param {Number} cx1
|
|
* @param {Number} cy1
|
|
* @param {Number} cx2
|
|
* @param {Number} cy2
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
bezierCurveTo: function (cx1, cy1, cx2, cy2, x, y) {
|
|
var me = this;
|
|
if (!me.cursor) {
|
|
me.moveTo(cx1, cy1);
|
|
}
|
|
me.coords.push(cx1, cy1, cx2, cy2, x, y);
|
|
me.types.push('C');
|
|
me.cursor[0] = x;
|
|
me.cursor[1] = y;
|
|
me.dirt();
|
|
},
|
|
|
|
/**
|
|
* A quadratic bezier curve to a position.
|
|
* @param {Number} cx
|
|
* @param {Number} cy
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
quadraticCurveTo: function (cx, cy, x, y) {
|
|
var me = this;
|
|
if (!me.cursor) {
|
|
me.moveTo(cx, cy);
|
|
}
|
|
me.bezierCurveTo(
|
|
(me.cursor[0] * 2 + cx) / 3, (me.cursor[1] * 2 + cy) / 3,
|
|
(x * 2 + cx) / 3, (y * 2 + cy) / 3,
|
|
x, y
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Close this path with a straight line.
|
|
*/
|
|
closePath: function () {
|
|
var me = this;
|
|
if (me.cursor) {
|
|
me.types.push('Z');
|
|
me.dirt();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create a elliptic arc curve compatible with SVG's arc to instruction.
|
|
*
|
|
* The curve start from (`x1`, `y1`) and ends at (`x2`, `y2`). The ellipse
|
|
* has radius `rx` and `ry` and a rotation of `rotation`.
|
|
* @param {Number} x1
|
|
* @param {Number} y1
|
|
* @param {Number} x2
|
|
* @param {Number} y2
|
|
* @param {Number} [rx]
|
|
* @param {Number} [ry]
|
|
* @param {Number} [rotation]
|
|
*/
|
|
arcTo: function (x1, y1, x2, y2, rx, ry, rotation) {
|
|
var me = this;
|
|
if (ry === undefined) {
|
|
ry = rx;
|
|
}
|
|
|
|
if (rotation === undefined) {
|
|
rotation = 0;
|
|
}
|
|
|
|
if (!me.cursor) {
|
|
me.moveTo(x1, y1);
|
|
return;
|
|
}
|
|
|
|
if (rx === 0 || ry === 0) {
|
|
me.lineTo(x1, y1);
|
|
return;
|
|
}
|
|
|
|
x2 -= x1;
|
|
y2 -= y1;
|
|
|
|
var x0 = me.cursor[0] - x1,
|
|
y0 = me.cursor[1] - y1,
|
|
area = x2 * y0 - y2 * x0,
|
|
cos, sin, xx, yx, xy, yy,
|
|
l0 = Math.sqrt(x0 * x0 + y0 * y0),
|
|
l2 = Math.sqrt(x2 * x2 + y2 * y2),
|
|
dist, cx, cy;
|
|
// cos rx, -sin ry , x1 - cos rx x1 + ry sin y1
|
|
// sin rx, cos ry, -rx sin x1 + y1 - cos ry y1
|
|
if (area === 0) {
|
|
me.lineTo(x1, y1);
|
|
return;
|
|
}
|
|
|
|
if (ry !== rx) {
|
|
cos = Math.cos(rotation);
|
|
sin = Math.sin(rotation);
|
|
xx = cos / rx;
|
|
yx = sin / ry;
|
|
xy = -sin / rx;
|
|
yy = cos / ry;
|
|
var temp = xx * x0 + yx * y0;
|
|
y0 = xy * x0 + yy * y0;
|
|
x0 = temp;
|
|
temp = xx * x2 + yx * y2;
|
|
y2 = xy * x2 + yy * y2;
|
|
x2 = temp;
|
|
} else {
|
|
x0 /= rx;
|
|
y0 /= ry;
|
|
x2 /= rx;
|
|
y2 /= ry;
|
|
}
|
|
|
|
cx = x0 * l2 + x2 * l0;
|
|
cy = y0 * l2 + y2 * l0;
|
|
dist = 1 / (Math.sin(Math.asin(Math.abs(area) / (l0 * l2)) * 0.5) * Math.sqrt(cx * cx + cy * cy));
|
|
cx *= dist;
|
|
cy *= dist;
|
|
|
|
var k0 = (cx * x0 + cy * y0) / (x0 * x0 + y0 * y0),
|
|
k2 = (cx * x2 + cy * y2) / (x2 * x2 + y2 * y2);
|
|
var cosStart = x0 * k0 - cx,
|
|
sinStart = y0 * k0 - cy,
|
|
cosEnd = x2 * k2 - cx,
|
|
sinEnd = y2 * k2 - cy,
|
|
startAngle = Math.atan2(sinStart, cosStart),
|
|
endAngle = Math.atan2(sinEnd, cosEnd);
|
|
if (area > 0) {
|
|
if (endAngle < startAngle) {
|
|
endAngle += Math.PI * 2;
|
|
}
|
|
} else {
|
|
if (startAngle < endAngle) {
|
|
startAngle += Math.PI * 2;
|
|
}
|
|
}
|
|
if (ry !== rx) {
|
|
cx = cos * cx * rx - sin * cy * ry + x1;
|
|
cy = sin * cy * ry + cos * cy * ry + y1;
|
|
me.lineTo(cos * rx * cosStart - sin * ry * sinStart + cx,
|
|
sin * rx * cosStart + cos * ry * sinStart + cy);
|
|
me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
|
|
} else {
|
|
cx = cx * rx + x1;
|
|
cy = cy * ry + y1;
|
|
me.lineTo(rx * cosStart + cx, ry * sinStart + cy);
|
|
me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create an elliptic arc.
|
|
*
|
|
* See [the whatwg reference of ellipse](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-ellipse).
|
|
*
|
|
* @param cx
|
|
* @param cy
|
|
* @param radiusX
|
|
* @param radiusY
|
|
* @param rotation
|
|
* @param startAngle
|
|
* @param endAngle
|
|
* @param anticlockwise
|
|
*/
|
|
ellipse: function (cx, cy, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
|
|
var me = this,
|
|
coords = me.coords,
|
|
start = coords.length, count,
|
|
i, j;
|
|
if (endAngle - startAngle >= Math.PI * 2) {
|
|
me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle, startAngle + Math.PI, anticlockwise);
|
|
me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle + Math.PI, endAngle, anticlockwise);
|
|
return;
|
|
}
|
|
if (!anticlockwise) {
|
|
if (endAngle < startAngle) {
|
|
endAngle += Math.PI * 2;
|
|
}
|
|
count = me.approximateArc(coords, cx, cy, radiusX, radiusY, rotation, startAngle, endAngle);
|
|
} else {
|
|
if (startAngle < endAngle) {
|
|
startAngle += Math.PI * 2;
|
|
}
|
|
count = me.approximateArc(coords, cx, cy, radiusX, radiusY, rotation, endAngle, startAngle);
|
|
for (i = start, j = coords.length - 2; i < j; i += 2, j -= 2) {
|
|
var temp = coords[i];
|
|
coords[i] = coords[j];
|
|
coords[j] = temp;
|
|
temp = coords[i + 1];
|
|
coords[i + 1] = coords[j + 1];
|
|
coords[j + 1] = temp;
|
|
}
|
|
}
|
|
|
|
if (!me.cursor) {
|
|
me.cursor = [coords[coords.length - 2], coords[coords.length - 1]];
|
|
me.types.push('M');
|
|
} else {
|
|
me.cursor[0] = coords[coords.length - 2];
|
|
me.cursor[1] = coords[coords.length - 1];
|
|
me.types.push('L');
|
|
}
|
|
|
|
for (i = 2; i < count; i += 6) {
|
|
me.types.push('C');
|
|
}
|
|
me.dirt();
|
|
},
|
|
|
|
/**
|
|
* Create an circular arc.
|
|
*
|
|
* @param x
|
|
* @param y
|
|
* @param radius
|
|
* @param startAngle
|
|
* @param endAngle
|
|
* @param anticlockwise
|
|
*/
|
|
arc: function (x, y, radius, startAngle, endAngle, anticlockwise) {
|
|
this.ellipse(x, y, radius, radius, 0, startAngle, endAngle, anticlockwise);
|
|
},
|
|
|
|
/**
|
|
* Draw a rectangle and close it.
|
|
*
|
|
* @param x
|
|
* @param y
|
|
* @param width
|
|
* @param height
|
|
*/
|
|
rect: function (x, y, width, height) {
|
|
var me = this;
|
|
me.moveTo(x, y);
|
|
me.lineTo(x + width, y);
|
|
me.lineTo(x + width, y + height);
|
|
me.lineTo(x, y + height);
|
|
me.closePath();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param result
|
|
* @param cx
|
|
* @param cy
|
|
* @param rx
|
|
* @param ry
|
|
* @param phi
|
|
* @param theta1
|
|
* @param theta2
|
|
* @return {Number}
|
|
*/
|
|
approximateArc: function (result, cx, cy, rx, ry, phi, theta1, theta2) {
|
|
var cosPhi = Math.cos(phi),
|
|
sinPhi = Math.sin(phi),
|
|
cosTheta1 = Math.cos(theta1),
|
|
sinTheta1 = Math.sin(theta1),
|
|
xx = cosPhi * cosTheta1 * rx - sinPhi * sinTheta1 * ry,
|
|
yx = -cosPhi * sinTheta1 * rx - sinPhi * cosTheta1 * ry,
|
|
xy = sinPhi * cosTheta1 * rx + cosPhi * sinTheta1 * ry,
|
|
yy = -sinPhi * sinTheta1 * rx + cosPhi * cosTheta1 * ry,
|
|
rightAngle = Math.PI / 2,
|
|
count = 2,
|
|
exx = xx,
|
|
eyx = yx,
|
|
exy = xy,
|
|
eyy = yy,
|
|
rho = 0.547443256150549,
|
|
temp, y1, x3, y3, x2, y2;
|
|
|
|
theta2 -= theta1;
|
|
if (theta2 < 0) {
|
|
theta2 += Math.PI * 2;
|
|
}
|
|
result.push(xx + cx, xy + cy);
|
|
while (theta2 >= rightAngle) {
|
|
result.push(
|
|
exx + eyx * rho + cx, exy + eyy * rho + cy,
|
|
exx * rho + eyx + cx, exy * rho + eyy + cy,
|
|
eyx + cx, eyy + cy
|
|
);
|
|
count += 6;
|
|
theta2 -= rightAngle;
|
|
temp = exx;
|
|
exx = eyx;
|
|
eyx = -temp;
|
|
temp = exy;
|
|
exy = eyy;
|
|
eyy = -temp;
|
|
}
|
|
if (theta2) {
|
|
y1 = (0.3294738052815987 + 0.012120855841304373 * theta2) * theta2;
|
|
x3 = Math.cos(theta2);
|
|
y3 = Math.sin(theta2);
|
|
x2 = x3 + y1 * y3;
|
|
y2 = y3 - y1 * x3;
|
|
result.push(
|
|
exx + eyx * y1 + cx, exy + eyy * y1 + cy,
|
|
exx * x2 + eyx * y2 + cx, exy * x2 + eyy * y2 + cy,
|
|
exx * x3 + eyx * y3 + cx, exy * x3 + eyy * y3 + cy
|
|
);
|
|
count += 6;
|
|
}
|
|
return count;
|
|
},
|
|
|
|
/**
|
|
* [http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes](http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes)
|
|
* @param rx
|
|
* @param ry
|
|
* @param rotation Differ from svg spec, this is radian.
|
|
* @param fA
|
|
* @param fS
|
|
* @param x2
|
|
* @param y2
|
|
*/
|
|
arcSvg: function (rx, ry, rotation, fA, fS, x2, y2) {
|
|
if (rx < 0) {
|
|
rx = -rx;
|
|
}
|
|
if (ry < 0) {
|
|
ry = -ry;
|
|
}
|
|
var me = this,
|
|
x1 = me.cursor[0],
|
|
y1 = me.cursor[1],
|
|
hdx = (x1 - x2) / 2,
|
|
hdy = (y1 - y2) / 2,
|
|
cosPhi = Math.cos(rotation),
|
|
sinPhi = Math.sin(rotation),
|
|
xp = hdx * cosPhi + hdy * sinPhi,
|
|
yp = -hdx * sinPhi + hdy * cosPhi,
|
|
ratX = xp / rx,
|
|
ratY = yp / ry,
|
|
lambda = ratX * ratX + ratY * ratY,
|
|
cx = (x1 + x2) * 0.5, cy = (y1 + y2) * 0.5,
|
|
cpx = 0, cpy = 0;
|
|
if (lambda >= 1) {
|
|
lambda = Math.sqrt(lambda);
|
|
rx *= lambda;
|
|
ry *= lambda;
|
|
// me gives lambda == cpx == cpy == 0;
|
|
} else {
|
|
lambda = Math.sqrt(1 / lambda - 1);
|
|
if (fA === fS) {
|
|
lambda = -lambda;
|
|
}
|
|
cpx = lambda * rx * ratY;
|
|
cpy = -lambda * ry * ratX;
|
|
cx += cosPhi * cpx - sinPhi * cpy;
|
|
cy += sinPhi * cpx + cosPhi * cpy;
|
|
}
|
|
|
|
var theta1 = Math.atan2((yp - cpy) / ry, (xp - cpx) / rx),
|
|
deltaTheta = Math.atan2((-yp - cpy) / ry, (-xp - cpx) / rx) - theta1;
|
|
|
|
if (fS) {
|
|
if (deltaTheta <= 0) {
|
|
deltaTheta += Math.PI * 2;
|
|
}
|
|
} else {
|
|
if (deltaTheta >= 0) {
|
|
deltaTheta -= Math.PI * 2;
|
|
}
|
|
}
|
|
me.ellipse(cx, cy, rx, ry, rotation, theta1, theta1 + deltaTheta, 1 - fS);
|
|
},
|
|
|
|
/**
|
|
* Feed the path from svg path string.
|
|
* @param pathString
|
|
*/
|
|
fromSvgString: function (pathString) {
|
|
if (!pathString) {
|
|
return;
|
|
}
|
|
var me = this,
|
|
parts,
|
|
paramCounts = {
|
|
a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0,
|
|
A: 7, C: 6, H: 1, L: 2, M: 2, Q: 4, S: 4, T: 2, V: 1, Z: 0
|
|
},
|
|
lastCommand = '',
|
|
lastControlX, lastControlY,
|
|
lastX = 0, lastY = 0,
|
|
part = false, i, partLength, relative;
|
|
|
|
// Split the string to items.
|
|
if (Ext.isString(pathString)) {
|
|
parts = pathString.replace(Ext.draw.Path.pathRe, " $1 ").replace(Ext.draw.Path.pathRe2, " -").split(Ext.draw.Path.pathSplitRe);
|
|
} else if (Ext.isArray(pathString)) {
|
|
parts = pathString.join(',').split(Ext.draw.Path.pathSplitRe);
|
|
}
|
|
|
|
// Remove empty entries
|
|
for (i = 0, partLength = 0; i < parts.length; i++) {
|
|
if (parts[i] !== '') {
|
|
parts[partLength++] = parts[i];
|
|
}
|
|
}
|
|
parts.length = partLength;
|
|
|
|
me.clear();
|
|
for (i = 0; i < parts.length;) {
|
|
lastCommand = part;
|
|
part = parts[i];
|
|
relative = (part.toUpperCase() !== part);
|
|
i++;
|
|
switch (part) {
|
|
case 'M':
|
|
me.moveTo(lastX = +parts[i], lastY = +parts[i + 1]);
|
|
i += 2;
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
|
|
i += 2;
|
|
}
|
|
break;
|
|
case 'L':
|
|
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
|
|
i += 2;
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
|
|
i += 2;
|
|
}
|
|
break;
|
|
case 'A':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.arcSvg(
|
|
+parts[i], +parts[i + 1],
|
|
+parts[i + 2] * Math.PI / 180,
|
|
+parts[i + 3], +parts[i + 4],
|
|
lastX = +parts[i + 5], lastY = +parts[i + 6]);
|
|
i += 7;
|
|
}
|
|
break;
|
|
case 'C':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.bezierCurveTo(
|
|
+parts[i ], +parts[i + 1],
|
|
lastControlX = +parts[i + 2], lastControlY = +parts[i + 3],
|
|
lastX = +parts[i + 4], lastY = +parts[i + 5]);
|
|
i += 6;
|
|
}
|
|
break;
|
|
case 'Z':
|
|
me.closePath();
|
|
break;
|
|
case 'm':
|
|
me.moveTo(lastX += +parts[i], lastY += +parts[i + 1]);
|
|
i += 2;
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
|
|
i += 2;
|
|
}
|
|
break;
|
|
case 'l':
|
|
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
|
|
i += 2;
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
|
|
i += 2;
|
|
}
|
|
break;
|
|
case 'a':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.arcSvg(
|
|
+parts[i], +parts[i + 1],
|
|
+parts[i + 2] * Math.PI / 180,
|
|
+parts[i + 3], +parts[i + 4],
|
|
lastX += +parts[i + 5], lastY += +parts[i + 6]);
|
|
i += 7;
|
|
}
|
|
break;
|
|
case 'c':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.bezierCurveTo(lastX + (+parts[i]), lastY + (+parts[i + 1]),
|
|
lastControlX = lastX + (+parts[i + 2]), lastControlY = lastY + (+parts[i + 3]),
|
|
lastX += +parts[i + 4], lastY += +parts[i + 5]);
|
|
i += 6;
|
|
}
|
|
break;
|
|
case 'z':
|
|
me.closePath();
|
|
break;
|
|
case 's':
|
|
if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
|
|
lastControlX = lastX;
|
|
lastControlY = lastY;
|
|
}
|
|
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.bezierCurveTo(
|
|
lastX + lastX - lastControlX, lastY + lastY - lastControlY,
|
|
lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]),
|
|
lastX += +parts[i + 2], lastY += +parts[i + 3]);
|
|
i += 4;
|
|
}
|
|
break;
|
|
case 'S':
|
|
if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
|
|
lastControlX = lastX;
|
|
lastControlY = lastY;
|
|
}
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.bezierCurveTo(
|
|
lastX + lastX - lastControlX, lastY + lastY - lastControlY,
|
|
lastControlX = +parts[i], lastControlY = +parts[i + 1],
|
|
lastX = (+parts[i + 2]), lastY = (+parts[i + 3]));
|
|
i += 4;
|
|
}
|
|
break;
|
|
case 'q':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.quadraticCurveTo(
|
|
lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]),
|
|
lastX += +parts[i + 2], lastY += +parts[i + 3]);
|
|
i += 4;
|
|
}
|
|
break;
|
|
case 'Q':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.quadraticCurveTo(
|
|
lastControlX = +parts[i], lastControlY = +parts[i + 1],
|
|
lastX = +parts[i + 2], lastY = +parts[i + 3]);
|
|
i += 4;
|
|
}
|
|
break;
|
|
case 't':
|
|
if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
|
|
lastControlX = lastX;
|
|
lastControlY = lastY;
|
|
}
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.quadraticCurveTo(
|
|
lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY,
|
|
lastX += +parts[i + 1], lastY += +parts[i + 2]);
|
|
i += 2;
|
|
}
|
|
break;
|
|
case 'T':
|
|
if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
|
|
lastControlX = lastX;
|
|
lastControlY = lastY;
|
|
}
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.quadraticCurveTo(
|
|
lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY,
|
|
lastX = (+parts[i + 1]), lastY = (+parts[i + 2]));
|
|
i += 2;
|
|
}
|
|
break;
|
|
case 'h':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.lineTo(lastX += +parts[i], lastY);
|
|
i++;
|
|
}
|
|
break;
|
|
case 'H':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.lineTo(lastX = +parts[i], lastY);
|
|
i++;
|
|
}
|
|
break;
|
|
case 'v':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.lineTo(lastX, lastY += +parts[i]);
|
|
i++;
|
|
}
|
|
break;
|
|
case 'V':
|
|
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
|
|
me.lineTo(lastX, lastY = +parts[i]);
|
|
i++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param x1
|
|
* @param y1
|
|
* @param x2
|
|
* @param y2
|
|
* @param x
|
|
* @param y
|
|
* @return {Number}
|
|
*/
|
|
rayTestLine: function (x1, y1, x2, y2, x, y) {
|
|
var cx;
|
|
if (y1 === y2) {
|
|
if (y === y1) {
|
|
if (Math.min(x1, x2) <= x && x <= Math.max(x1, x2)) {
|
|
return -1;
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
if (y1 < y && y < y2 || y2 < y && y < y1) {
|
|
cx = (y - y1) * (x2 - x1) / (y2 - y1) + x1;
|
|
if (cx === x) {
|
|
return -1;
|
|
} else if (cx < x) {
|
|
return 0;
|
|
} else {
|
|
return 1;
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param x1
|
|
* @param y1
|
|
* @param x2
|
|
* @param y2
|
|
* @param x3
|
|
* @param y3
|
|
* @param x4
|
|
* @param y4
|
|
* @param x
|
|
* @param y
|
|
* @return {*}
|
|
*/
|
|
rayTestCubicBezier: function (x1, y1, x2, y2, x3, y3, x4, y4, x, y, idx) {
|
|
if (Math.min(x1, x2, x3, x4) <= x && x <= Math.max(x1, x2, x3, x4)) {
|
|
if (Math.min(y1, y2, y3, y4) <= y && y <= Math.max(y1, y2, y3, y4)) {
|
|
var me = this,
|
|
solver = me.solvers[idx] || (me.solvers[idx] = Ext.draw.Solver.createBezierSolver(x1, x2, x3, x4)),
|
|
result = solver.solve(y);
|
|
return (+(x <= result[0] && 0 <= result[0] && result[0] <= 1)) +
|
|
(+(x <= result[1] && 0 <= result[1] && result[1] <= 1)) +
|
|
(+(x <= result[2] && 0 <= result[2] && result[2] <= 1));
|
|
}
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* Test wether the given point is on or inside the path.
|
|
* @param x
|
|
* @param y
|
|
* @return {Boolean}
|
|
*/
|
|
isPointInPath: function (x, y) {
|
|
var me = this,
|
|
i, j, count = 0, test = 0,
|
|
types = me.types,
|
|
coords = me.coords,
|
|
ln = types.length, firstX = null, firstY = null, lastX = 0, lastY = 0;
|
|
for (i = 0, j = 0; i < ln; i++) {
|
|
switch (types[i]) {
|
|
case 'M':
|
|
if (firstX !== null) {
|
|
test = me.rayTestLine(firstX, firstY, lastX, lastY, x, y);
|
|
if (test < 0) {
|
|
count += 1;
|
|
} else {
|
|
count += test;
|
|
}
|
|
}
|
|
firstX = lastX = coords[j];
|
|
firstY = lastY = coords[j + 1];
|
|
j += 2;
|
|
break;
|
|
case 'L':
|
|
test = me.rayTestLine(lastX, lastY, coords[j], coords[j + 1], x, y);
|
|
if (test < 0) {
|
|
return true;
|
|
}
|
|
count += test;
|
|
lastX = coords[j];
|
|
lastY = coords[j + 1];
|
|
j += 2;
|
|
break;
|
|
case 'C':
|
|
test = me.rayTestCubicBezier(
|
|
lastX, lastY,
|
|
coords[j], coords[j + 1],
|
|
coords[j + 2], coords[j + 3],
|
|
coords[j + 4], coords[j + 5],
|
|
x, y, i);
|
|
if (test < 0) {
|
|
return true;
|
|
}
|
|
count += test;
|
|
lastX = coords[j + 4];
|
|
lastY = coords[j + 5];
|
|
j += 6;
|
|
break;
|
|
case 'Z':
|
|
break;
|
|
}
|
|
}
|
|
return count % 2 === 1;
|
|
},
|
|
|
|
/**
|
|
* Clone this path.
|
|
* @return {Ext.draw.Path}
|
|
*/
|
|
clone: function () {
|
|
var me = this,
|
|
path = new Ext.draw.Path();
|
|
path.coords = me.coords.slice(0);
|
|
path.types = me.types.slice(0);
|
|
path.cursor = me.cursor ? me.cursor.slice(0) : null;
|
|
path.startX = me.startX;
|
|
path.startY = me.startY;
|
|
path.svgString = me.svgString;
|
|
return path;
|
|
},
|
|
|
|
/**
|
|
* Transform the current path by a matrix.
|
|
* @param {Ext.draw.Matrix} matrix
|
|
*/
|
|
transform: function (matrix) {
|
|
if (matrix.isIdentity()) {
|
|
return;
|
|
}
|
|
var xx = matrix.getXX(), yx = matrix.getYX(), dx = matrix.getDX(),
|
|
xy = matrix.getXY(), yy = matrix.getYY(), dy = matrix.getDY(),
|
|
coords = this.coords,
|
|
i = 0, ln = coords.length,
|
|
x, y;
|
|
|
|
for (; i < ln; i += 2) {
|
|
x = coords[i];
|
|
y = coords[i + 1];
|
|
coords[i] = x * xx + y * yx + dx;
|
|
coords[i + 1] = x * xy + y * yy + dy;
|
|
}
|
|
this.dirt();
|
|
},
|
|
|
|
/**
|
|
* Get the bounding box of this matrix.
|
|
* @param {Object} [target] Optional object to receive the result.
|
|
*
|
|
* @return {Object} Object with x, y, width and height
|
|
*/
|
|
getDimension: function (target) {
|
|
if (!target) {
|
|
target = {};
|
|
}
|
|
|
|
if (!this.types || !this.types.length) {
|
|
target.x = 0;
|
|
target.y = 0;
|
|
target.width = 0;
|
|
target.height = 0;
|
|
return target;
|
|
}
|
|
|
|
target.left = Infinity;
|
|
target.top = Infinity;
|
|
target.right = -Infinity;
|
|
target.bottom = -Infinity;
|
|
var i = 0, j = 0,
|
|
types = this.types,
|
|
coords = this.coords,
|
|
ln = types.length, x, y;
|
|
for (; i < ln; i++) {
|
|
switch (types[i]) {
|
|
case 'M':
|
|
case 'L':
|
|
x = coords[j];
|
|
y = coords[j + 1];
|
|
target.left = Math.min(x, target.left);
|
|
target.top = Math.min(y, target.top);
|
|
target.right = Math.max(x, target.right);
|
|
target.bottom = Math.max(y, target.bottom);
|
|
j += 2;
|
|
break;
|
|
case 'C':
|
|
this.expandDimension(target, x, y,
|
|
coords[j], coords[j + 1],
|
|
coords[j + 2], coords[j + 3],
|
|
x = coords[j + 4], y = coords[j + 5]);
|
|
j += 6;
|
|
break;
|
|
}
|
|
}
|
|
|
|
target.x = target.left;
|
|
target.y = target.top;
|
|
target.width = target.right - target.left;
|
|
target.height = target.bottom - target.top;
|
|
return target;
|
|
},
|
|
|
|
/**
|
|
* Get the bounding box as if the path is transformed by a matrix.
|
|
*
|
|
* @param {Ext.draw.Matrix} matrix
|
|
* @param {Object} [target] Optional object to receive the result.
|
|
*
|
|
* @return {Object} An object with x, y, width and height.
|
|
*/
|
|
getDimensionWithTransform: function (matrix, target) {
|
|
if (!this.types || !this.types.length) {
|
|
if (!target) {
|
|
target = {};
|
|
}
|
|
target.x = 0;
|
|
target.y = 0;
|
|
target.width = 0;
|
|
target.height = 0;
|
|
return target;
|
|
}
|
|
|
|
target.left = Infinity;
|
|
target.top = Infinity;
|
|
target.right = -Infinity;
|
|
target.bottom = -Infinity;
|
|
|
|
var xx = matrix.getXX(), yx = matrix.getYX(), dx = matrix.getDX(),
|
|
xy = matrix.getXY(), yy = matrix.getYY(), dy = matrix.getDY(),
|
|
i = 0, j = 0,
|
|
types = this.types,
|
|
coords = this.coords,
|
|
ln = types.length, x, y;
|
|
for (; i < ln; i++) {
|
|
switch (types[i]) {
|
|
case 'M':
|
|
case 'L':
|
|
x = coords[j] * xx + coords[j + 1] * yx + dx;
|
|
y = coords[j] * xy + coords[j + 1] * yy + dy;
|
|
target.left = Math.min(x, target.left);
|
|
target.top = Math.min(y, target.top);
|
|
target.right = Math.max(x, target.right);
|
|
target.bottom = Math.max(y, target.bottom);
|
|
j += 2;
|
|
break;
|
|
case 'C':
|
|
this.expandDimension(target,
|
|
x, y,
|
|
coords[j] * xx + coords[j + 1] * yx + dx,
|
|
coords[j] * xy + coords[j + 1] * yy + dy,
|
|
coords[j + 2] * xx + coords[j + 3] * yx + dx,
|
|
coords[j + 2] * xy + coords[j + 3] * yy + dy,
|
|
x = coords[j + 4] * xx + coords[j + 5] * yx + dx,
|
|
y = coords[j + 4] * xy + coords[j + 5] * yy + dy);
|
|
j += 6;
|
|
break;
|
|
}
|
|
}
|
|
if (!target) {
|
|
target = {};
|
|
}
|
|
target.x = target.left;
|
|
target.y = target.top;
|
|
target.width = target.right - target.left;
|
|
target.height = target.bottom - target.top;
|
|
return target;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* Expand the rect by the bbox of a bezier curve.
|
|
*
|
|
* @param target
|
|
* @param x1
|
|
* @param y1
|
|
* @param cx1
|
|
* @param cy1
|
|
* @param cx2
|
|
* @param cy2
|
|
* @param x2
|
|
* @param y2
|
|
*/
|
|
expandDimension: function (target, x1, y1, cx1, cy1, cx2, cy2, x2, y2) {
|
|
var me = this,
|
|
l = target.left, r = target.right, t = target.top, b = target.bottom,
|
|
dim = me.dim || (me.dim = []);
|
|
|
|
me.curveDimension(x1, cx1, cx2, x2, dim);
|
|
l = Math.min(l, dim[0]);
|
|
r = Math.max(r, dim[1]);
|
|
|
|
me.curveDimension(y1, cy1, cy2, y2, dim);
|
|
t = Math.min(t, dim[0]);
|
|
b = Math.max(b, dim[1]);
|
|
|
|
target.left = l;
|
|
target.right = r;
|
|
target.top = t;
|
|
target.bottom = b;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* Determin the
|
|
* @param a
|
|
* @param b
|
|
* @param c
|
|
* @param d
|
|
* @param dim
|
|
*/
|
|
curveDimension: function (a, b, c, d, dim) {
|
|
var qa = 3 * (-a + 3 * (b - c) + d),
|
|
qb = 6 * (a - 2 * b + c),
|
|
qc = -3 * (a - b), x, y,
|
|
min = Math.min(a, d),
|
|
max = Math.max(a, d), delta;
|
|
|
|
if (qa === 0) {
|
|
if (qb === 0) {
|
|
dim[0] = min;
|
|
dim[1] = max;
|
|
return;
|
|
} else {
|
|
x = -qc / qb;
|
|
if (0 < x && x < 1) {
|
|
y = this.interpolate(a, b, c, d, x);
|
|
min = Math.min(min, y);
|
|
max = Math.max(max, y);
|
|
}
|
|
}
|
|
} else {
|
|
delta = qb * qb - 4 * qa * qc;
|
|
if (delta >= 0) {
|
|
delta = Math.sqrt(delta);
|
|
x = (delta - qb) / 2 / qa;
|
|
if (0 < x && x < 1) {
|
|
y = this.interpolate(a, b, c, d, x);
|
|
min = Math.min(min, y);
|
|
max = Math.max(max, y);
|
|
}
|
|
if (delta > 0) {
|
|
x -= delta / qa;
|
|
if (0 < x && x < 1) {
|
|
y = this.interpolate(a, b, c, d, x);
|
|
min = Math.min(min, y);
|
|
max = Math.max(max, y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
dim[0] = min;
|
|
dim[1] = max;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*
|
|
* Returns `a * (1 - t) ^ 3 + 3 * b (1 - t) ^ 2 * t + 3 * c (1 - t) * t ^ 3 + d * t ^ 3`.
|
|
*
|
|
* @param a
|
|
* @param b
|
|
* @param c
|
|
* @param d
|
|
* @param t
|
|
* @return {Number}
|
|
*/
|
|
interpolate: function (a, b, c, d, t) {
|
|
if (t === 0) {
|
|
return a;
|
|
}
|
|
if (t === 1) {
|
|
return d;
|
|
}
|
|
var rate = (1 - t) / t;
|
|
return t * t * t * (d + rate * (3 * c + rate * (3 * b + rate * a)));
|
|
},
|
|
|
|
/**
|
|
* Reconstruct path from cubic bezier curve stripes.
|
|
* @param {Array} stripes
|
|
*/
|
|
fromStripes: function (stripes) {
|
|
var me = this,
|
|
i = 0, ln = stripes.length,
|
|
j, ln2, stripe;
|
|
me.clear();
|
|
for (; i < ln; i++) {
|
|
stripe = stripes[i];
|
|
me.coords.push.apply(me.coords, stripe);
|
|
me.types.push('M');
|
|
for (j = 2, ln2 = stripe.length; j < ln2; j += 6) {
|
|
me.types.push('C');
|
|
}
|
|
}
|
|
if (!me.cursor) {
|
|
me.cursor = [];
|
|
}
|
|
me.cursor[0] = me.coords[me.coords.length - 2];
|
|
me.cursor[1] = me.coords[me.coords.length - 1];
|
|
me.dirt();
|
|
},
|
|
|
|
/**
|
|
* Convert path to bezier curve stripes.
|
|
* @param {Array} [target] The optional array to receive the result.
|
|
* @return {Array}
|
|
*/
|
|
toStripes: function (target) {
|
|
var stripes = target || [], curr,
|
|
x, y, lastX, lastY, startX, startY,
|
|
i, j,
|
|
types = this.types,
|
|
coords = this.coords,
|
|
ln = types.length;
|
|
for (i = 0, j = 0; i < ln; i++) {
|
|
switch (types[i]) {
|
|
case 'M':
|
|
curr = [startX = lastX = coords[j++], startY = lastY = coords[j++]];
|
|
stripes.push(curr);
|
|
break;
|
|
case 'L':
|
|
x = coords[j++];
|
|
y = coords[j++];
|
|
curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
|
|
break;
|
|
case 'C':
|
|
curr.push(coords[j++], coords[j++], coords[j++], coords[j++], lastX = coords[j++], lastY = coords[j++]);
|
|
break;
|
|
case 'Z':
|
|
x = startX;
|
|
y = startY;
|
|
curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
|
|
break;
|
|
}
|
|
}
|
|
return stripes;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* Update cache for svg string of this path.
|
|
*/
|
|
updateSvgString: function () {
|
|
var result = [],
|
|
types = this.types,
|
|
coords = this.coords,
|
|
ln = types.length,
|
|
i = 0, j = 0;
|
|
for (; i < ln; i++) {
|
|
switch (types[i]) {
|
|
case 'M':
|
|
result.push('M' + coords[j] + ',' + coords[j + 1]);
|
|
j += 2;
|
|
break;
|
|
case 'L':
|
|
result.push('L' + coords[j] + ',' + coords[j + 1]);
|
|
j += 2;
|
|
break;
|
|
case 'C':
|
|
result.push('C' + coords[j] + ',' + coords[j + 1] + ' ' +
|
|
coords[j + 2] + ',' + coords[j + 3] + ' ' +
|
|
coords[j + 4] + ',' + coords[j + 5]);
|
|
j += 6;
|
|
break;
|
|
case 'Z':
|
|
result.push('Z');
|
|
break;
|
|
}
|
|
}
|
|
this.svgString = result.join('');
|
|
},
|
|
|
|
/**
|
|
* Return an svg path string for this path.
|
|
* @return {String}
|
|
*/
|
|
toString: function () {
|
|
if (!this.svgString) {
|
|
this.updateSvgString();
|
|
}
|
|
return this.svgString;
|
|
}
|
|
}); |