split up methods from the PathHelper class and moved them to Path and Geometry. Path specific methods went to Path, and general purpose geometric utility methods went to the Geometry utility class. TextPath now inherits methods from Path

This commit is contained in:
Eric Rowell 2012-07-28 16:08:14 -07:00
parent c4a359cd48
commit 9631d6e1bb
13 changed files with 4024 additions and 3880 deletions

View File

@ -4,7 +4,7 @@ class Build < Thor
# This is the list of files to concatenate. The first file will appear at the top of the final file. All files are relative to the lib directory.
FILES = [
"license.js", "src/Global.js", "src/Transition.js", "src/filters/Grayscale.js",
"src/util/Type.js", "src/util/Canvas.js", "src/util/Class.js", "src/util/Tween.js", "src/util/Transform.js",
"src/util/Type.js", "src/util/Canvas.js", "src/util/Class.js", "src/util/Tween.js", "src/util/Transform.js", "src/util/Geometry.js",
"src/Animation.js", "src/Node.js", "src/Container.js", "src/Stage.js", "src/Layer.js", "src/Group.js", "src/Shape.js",
"src/shapes/Rect.js", "src/shapes/Ellipse.js", "src/shapes/Image.js", "src/shapes/Polygon.js", "src/shapes/Text.js", "src/shapes/Line.js", "src/shapes/Sprite.js"
]

107
dist/kinetic-core.js vendored
View File

@ -1083,6 +1083,109 @@ Kinetic.Transform.prototype = {
}
};
/*
* Utility methods written by jfollas to
* handle length and point measurements
*/
Kinetic.Geometry = {
getLineLength: function(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
},
getPointOnLine: function(dist, P1x, P1y, P2x, P2y, fromX, fromY) {
if(fromX === undefined) {
fromX = P1x;
}
if(fromY === undefined) {
fromY = P1y;
}
var m = (P2y - P1y) / ((P2x - P1x) + 0.00000001);
var run = Math.sqrt(dist * dist / (1 + m * m));
var rise = m * run;
var pt;
if((fromY - P1y) / ((fromX - P1x) + 0.00000001) === m) {
pt = {
x: fromX + run,
y: fromY + rise
};
}
else {
var ix, iy;
var len = this.getLineLength(P1x, P1y, P2x, P2y);
if(len < 0.00000001) {
return undefined;
}
var u = (((fromX - P1x) * (P2x - P1x)) + ((fromY - P1y) * (P2y - P1y)));
u = u / (len * len);
ix = P1x + u * (P2x - P1x);
iy = P1y + u * (P2y - P1y);
var pRise = this.getLineLength(fromX, fromY, ix, iy);
var pRun = Math.sqrt(dist * dist - pRise * pRise);
run = Math.sqrt(pRun * pRun / (1 + m * m));
rise = m * run;
pt = {
x: ix + run,
y: iy + rise
};
}
return pt;
},
getPointOnCubicBezier: function(pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) {
function CB1(t) {
return t * t * t;
}
function CB2(t) {
return 3 * t * t * (1 - t);
}
function CB3(t) {
return 3 * t * (1 - t) * (1 - t);
}
function CB4(t) {
return (1 - t) * (1 - t) * (1 - t);
}
var x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct);
var y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct);
return {
x: x,
y: y
};
},
getPointOnQuadraticBezier: function(pct, P1x, P1y, P2x, P2y, P3x, P3y) {
function QB1(t) {
return t * t;
}
function QB2(t) {
return 2 * t * (1 - t);
}
function QB3(t) {
return (1 - t) * (1 - t);
}
var x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct);
var y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct);
return {
x: x,
y: y
};
},
getPointOnEllipticalArc: function(cx, cy, rx, ry, theta, psi) {
var cosPsi = Math.cos(psi), sinPsi = Math.sin(psi);
var pt = {
x: rx * Math.cos(theta),
y: ry * Math.sin(theta)
};
return {
x: cx + (pt.x * cosPsi - pt.y * sinPsi),
y: cy + (pt.x * sinPsi + pt.y * cosPsi)
};
}
};
///////////////////////////////////////////////////////////////////////
// Animation
///////////////////////////////////////////////////////////////////////
@ -4559,9 +4662,7 @@ Kinetic.Ellipse = Kinetic.Shape.extend({
// call super constructor
this._super(config);
this._convertRadius();
var that = this;
this.on('radiusChange.kinetic', function() {
that._convertRadius();
@ -4891,7 +4992,6 @@ Kinetic.Text = Kinetic.Shape.extend({
var attr = attrs[n];
this.on(attr + 'Change.kinetic', that._setTextData);
}
that._setTextData();
},
drawFunc: function(context) {
@ -5403,7 +5503,6 @@ Kinetic.Sprite = Kinetic.Shape.extend({
config.drawFunc = this.drawFunc;
// call super constructor
this._super(config);
var that = this;
this.on('animationChange.kinetic', function() {
// reset index when animation changes

File diff suppressed because one or more lines are too long

View File

@ -15,14 +15,11 @@ Kinetic.Plugins.Path = Kinetic.Shape.extend({
var that = this;
config.drawFunc = this.drawFunc;
// call super constructor
this._super(config);
this.dataArray = Kinetic.Plugins.PathHelper.parsePathData(this.attrs.data);
this.dataArray = this.parsePathData(this.attrs.data);
this.on('dataChange', function() {
that.dataArray = Kinetic.Plugins.PathHelper.parsePathData(that.attrs.data);
that.dataArray = this.parsePathData(that.attrs.data);
});
},
drawFunc: function(context) {
@ -68,6 +65,368 @@ Kinetic.Plugins.Path = Kinetic.Shape.extend({
}
this.fill(context);
this.stroke(context);
},
/**
* get parsed data array from the data
* string. V, v, H, h, and l data are converted to
* L data for the purpose of high performance Path
* rendering
*/
parsePathData: function(data) {
// Path Data Segment must begin with a moveTo
//m (x y)+ Relative moveTo (subsequent points are treated as lineTo)
//M (x y)+ Absolute moveTo (subsequent points are treated as lineTo)
//l (x y)+ Relative lineTo
//L (x y)+ Absolute LineTo
//h (x)+ Relative horizontal lineTo
//H (x)+ Absolute horizontal lineTo
//v (y)+ Relative vertical lineTo
//V (y)+ Absolute vertical lineTo
//z (closepath)
//Z (closepath)
//c (x1 y1 x2 y2 x y)+ Relative Bezier curve
//C (x1 y1 x2 y2 x y)+ Absolute Bezier curve
//q (x1 y1 x y)+ Relative Quadratic Bezier
//Q (x1 y1 x y)+ Absolute Quadratic Bezier
//t (x y)+ Shorthand/Smooth Relative Quadratic Bezier
//T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier
//s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve
//S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve
//a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc
//A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc
// return early if data is not defined
if(!data) {
return [];
}
// command string
var cs = data;
// command chars
var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'];
// convert white spaces to commas
cs = cs.replace(new RegExp(' ', 'g'), ',');
// create pipes so that we can split the data
for(var n = 0; n < cc.length; n++) {
cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
}
// create array
var arr = cs.split('|');
var ca = [];
// init context point
var cpx = 0;
var cpy = 0;
for(var n = 1; n < arr.length; n++) {
var str = arr[n];
var c = str.charAt(0);
str = str.slice(1);
// remove ,- for consistency
str = str.replace(new RegExp(',-', 'g'), '-');
// add commas so that it's easy to split
str = str.replace(new RegExp('-', 'g'), ',-');
str = str.replace(new RegExp('e,-', 'g'), 'e-');
var p = str.split(',');
if(p.length > 0 && p[0] === '') {
p.shift();
}
// convert strings to floats
for(var i = 0; i < p.length; i++) {
p[i] = parseFloat(p[i]);
}
while(p.length > 0) {
if(isNaN(p[0]))// case for a trailing comma before next command
break;
var cmd = null;
var points = [];
var startX = cpx, startY = cpy;
// convert l, H, h, V, and v to L
switch (c) {
// Note: Keep the lineTo's above the moveTo's in this switch
case 'l':
cpx += p.shift();
cpy += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'L':
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
// Note: lineTo handlers need to be above this point
case 'm':
cpx += p.shift();
cpy += p.shift();
cmd = 'M';
points.push(cpx, cpy);
c = 'l';
// subsequent points are treated as relative lineTo
break;
case 'M':
cpx = p.shift();
cpy = p.shift();
cmd = 'M';
points.push(cpx, cpy);
c = 'L';
// subsequent points are treated as absolute lineTo
break;
case 'h':
cpx += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'H':
cpx = p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'v':
cpy += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'V':
cpy = p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'C':
points.push(p.shift(), p.shift(), p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
case 'c':
points.push(cpx + p.shift(), cpy + p.shift(), cpx + p.shift(), cpy + p.shift());
cpx += p.shift();
cpy += p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
case 'S':
var ctlPtx = cpx, ctlPty = cpy;
var prevCmd = ca[ca.length - 1];
if(prevCmd.command === 'C') {
ctlPtx = cpx + (cpx - prevCmd.points[2]);
ctlPty = cpy + (cpy - prevCmd.points[3]);
}
points.push(ctlPtx, ctlPty, p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
case 's':
var ctlPtx = cpx, ctlPty = cpy;
var prevCmd = ca[ca.length - 1];
if(prevCmd.command === 'C') {
ctlPtx = cpx + (cpx - prevCmd.points[2]);
ctlPty = cpy + (cpy - prevCmd.points[3]);
}
points.push(ctlPtx, ctlPty, cpx + p.shift(), cpy + p.shift());
cpx += p.shift();
cpy += p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
case 'Q':
points.push(p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
case 'q':
points.push(cpx + p.shift(), cpy + p.shift());
cpx += p.shift();
cpy += p.shift();
cmd = 'Q';
points.push(cpx, cpy);
break;
case 'T':
var ctlPtx = cpx, ctlPty = cpy;
var prevCmd = ca[ca.length - 1];
if(prevCmd.command === 'Q') {
ctlPtx = cpx + (cpx - prevCmd.points[0]);
ctlPty = cpy + (cpy - prevCmd.points[1]);
}
cpx = p.shift();
cpy = p.shift();
cmd = 'Q';
points.push(ctlPtx, ctlPty, cpx, cpy);
break;
case 't':
var ctlPtx = cpx, ctlPty = cpy;
var prevCmd = ca[ca.length - 1];
if(prevCmd.command === 'Q') {
ctlPtx = cpx + (cpx - prevCmd.points[0]);
ctlPty = cpy + (cpy - prevCmd.points[1]);
}
cpx += p.shift();
cpy += p.shift();
cmd = 'Q';
points.push(ctlPtx, ctlPty, cpx, cpy);
break;
case 'A':
var rx = p.shift(), ry = p.shift(), psi = p.shift(), fa = p.shift(), fs = p.shift();
var x1 = cpx, y1 = cpy; cpx = p.shift(), cpy = p.shift();
cmd = 'A';
points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi);
break;
case 'a':
var rx = p.shift(), ry = p.shift(), psi = p.shift(), fa = p.shift(), fs = p.shift();
var x1 = cpx, y1 = cpy; cpx += p.shift(), cpy += p.shift();
cmd = 'A';
points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi);
break;
}
ca.push({
command: cmd || c,
points: points,
start: {
x: startX,
y: startY
},
pathLength: this.calcLength(startX, startY, cmd || c, points)
});
}
if(c === 'z' || c === 'Z')
ca.push({
command: 'z',
points: [],
start: undefined,
pathLength: 0
});
}
return ca;
},
calcLength: function(x, y, cmd, points) {
var len, p1, p2;
var g = Kinetic.Geometry;
switch (cmd) {
case 'L':
return g.getLineLength(x, y, points[0], points[1]);
case 'C':
// Approximates by breaking curve into 100 line segments
len = 0.0;
p1 = g.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
for( t = 0.01; t <= 1; t += 0.01) {
p2 = g.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
len += g.getLineLength(p1.x, p1.y, p2.x, p2.y);
p1 = p2;
}
return len;
case 'Q':
// Approximates by breaking curve into 100 line segments
len = 0.0;
p1 = g.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]);
for( t = 0.01; t <= 1; t += 0.01) {
p2 = g.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]);
len += g.getLineLength(p1.x, p1.y, p2.x, p2.y);
p1 = p2;
}
return len;
case 'A':
// Approximates by breaking curve into line segments
len = 0.0;
var start = points[4];
// 4 = theta
var dTheta = points[5];
// 5 = dTheta
var end = points[4] + dTheta;
var inc = Math.PI / 180.0;
// 1 degree resolution
if(Math.abs(start - end) < inc) {
inc = Math.abs(start - end);
}
// Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi
p1 = g.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0);
if(dTheta < 0) {// clockwise
for( t = start - inc; t > end; t -= inc) {
p2 = g.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
len += g.getLineLength(p1.x, p1.y, p2.x, p2.y);
p1 = p2;
}
}
else {// counter-clockwise
for( t = start + inc; t < end; t += inc) {
p2 = g.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
len += g.getLineLength(p1.x, p1.y, p2.x, p2.y);
p1 = p2;
}
}
p2 = g.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0);
len += g.getLineLength(p1.x, p1.y, p2.x, p2.y);
return len;
}
return 0;
},
convertEndpointToCenterParameterization: function(x1, y1, x2, y2, fa, fs, rx, ry, psiDeg) {
// Derived from: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
var psi = psiDeg * (Math.PI / 180.0);
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 f = Math.sqrt((((rx * rx) * (ry * ry)) - ((rx * rx) * (yp * yp)) - ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp) + (ry * ry) * (xp * xp)));
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 vMag = function(v) {
return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
};
var vRatio = function(u, v) {
return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v));
};
var vAngle = function(u, v) {
return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v));
};
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 [cx, cy, rx, ry, theta, dTheta, psi, fs];
}
});

View File

@ -10,7 +10,6 @@
*/
Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
init: function(config) {
this.setDefaultAttrs({
fontFamily: 'Calibri',
fontSize: 12,
@ -25,14 +24,11 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
var that = this;
config.drawFunc = this.drawFunc;
// call super constructor
this._super(config);
this.dataArray = Kinetic.Plugins.PathHelper.parsePathData(this.attrs.data);
this.dataArray = this.parsePathData(this.attrs.data);
this.on('dataChange', function() {
that.dataArray = Kinetic.Plugins.PathHelper.parsePathData(that.attrs.data);
that.dataArray = this.parsePathData(this.attrs.data);
});
// update text data for certain attr changes
var attrs = ['text', 'textStroke', 'textStrokeWidth'];
@ -40,7 +36,6 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
var attr = attrs[n];
this.on(attr + 'Change', that._setTextData);
}
that._setTextData();
},
drawFunc: function(context) {
@ -83,6 +78,9 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
context.restore();
},
parsePathData: Kinetic.Plugins.Path.prototype.parsePathData,
calcLength: Kinetic.Plugins.Path.prototype.calcLength,
convertEndpointToCenterParameterization: Kinetic.Plugins.Path.prototype.convertEndpointToCenterParameterization,
/**
* get text width in pixels
* @name getTextWidth
@ -181,8 +179,8 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
switch (pathCmd.command) {
case 'L':
if(Kinetic.Plugins.PathHelper.getLineLength(p0.x, p0.y, pathCmd.points[0], pathCmd.points[1]) > glyphWidth) {
p1 = Kinetic.Plugins.PathHelper.getPointOnLine(glyphWidth, p0.x, p0.y, pathCmd.points[0], pathCmd.points[1], p0.x, p0.y);
if(Kinetic.Geometry.getLineLength(p0.x, p0.y, pathCmd.points[0], pathCmd.points[1]) > glyphWidth) {
p1 = Kinetic.Geometry.getPointOnLine(glyphWidth, p0.x, p0.y, pathCmd.points[0], pathCmd.points[1], p0.x, p0.y);
}
else
pathCmd = undefined;
@ -207,7 +205,7 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
currentT = end;
needNewSegment = true;
}
p1 = Kinetic.Plugins.PathHelper.getPointOnEllipticalArc(pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], currentT, pathCmd.points[6]);
p1 = Kinetic.Geometry.getPointOnEllipticalArc(pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], currentT, pathCmd.points[6]);
break;
case 'C':
if(currentT === 0) {
@ -225,7 +223,7 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
currentT = 1.0;
needNewSegment = true;
}
p1 = Kinetic.Plugins.PathHelper.getPointOnCubicBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], pathCmd.points[4], pathCmd.points[5]);
p1 = Kinetic.Geometry.getPointOnCubicBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], pathCmd.points[4], pathCmd.points[5]);
break;
case 'Q':
if(currentT === 0)
@ -239,13 +237,13 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
currentT = 1.0;
needNewSegment = true;
}
p1 = Kinetic.Plugins.PathHelper.getPointOnQuadraticBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3]);
p1 = Kinetic.Geometry.getPointOnQuadraticBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3]);
break;
}
if(p1 !== undefined) {
currLen = Kinetic.Plugins.PathHelper.getLineLength(p0.x, p0.y, p1.x, p1.y);
currLen = Kinetic.Geometry.getLineLength(p0.x, p0.y, p1.x, p1.y);
}
if(needNewSegment) {
@ -262,7 +260,7 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
if(p0 === undefined || p1 === undefined)
break;
var width = Kinetic.Plugins.PathHelper.getLineLength(p0.x, p0.y, p1.x, p1.y);
var width = Kinetic.Geometry.getLineLength(p0.x, p0.y, p1.x, p1.y);
// Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used.
// Can foresee having a rough pair table built in that the developer can override as needed.
@ -270,7 +268,7 @@ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({
var kern = 0;
// placeholder for future implementation
var midpoint = Kinetic.Plugins.PathHelper.getPointOnLine(kern + width / 2.0, p0.x, p0.y, p1.x, p1.y);
var midpoint = Kinetic.Geometry.getPointOnLine(kern + width / 2.0, p0.x, p0.y, p1.x, p1.y);
var rotation = Math.atan2((p1.y - p0.y), (p1.x - p0.x));
this.glyphInfo.push({

View File

@ -1,495 +0,0 @@
///////////////////////////////////////////////////////////////////////
// PathHelper
///////////////////////////////////////////////////////////////////////
Kinetic.Plugins.PathHelper = {
/**
* get parsed data array from the data
* string. V, v, H, h, and l data are converted to
* L data for the purpose of high performance Path
* rendering
*/
parsePathData : function (data) {
// Path Data Segment must begin with a moveTo
//m (x y)+ Relative moveTo (subsequent points are treated as lineTo)
//M (x y)+ Absolute moveTo (subsequent points are treated as lineTo)
//l (x y)+ Relative lineTo
//L (x y)+ Absolute LineTo
//h (x)+ Relative horizontal lineTo
//H (x)+ Absolute horizontal lineTo
//v (y)+ Relative vertical lineTo
//V (y)+ Absolute vertical lineTo
//z (closepath)
//Z (closepath)
//c (x1 y1 x2 y2 x y)+ Relative Bezier curve
//C (x1 y1 x2 y2 x y)+ Absolute Bezier curve
//q (x1 y1 x y)+ Relative Quadratic Bezier
//Q (x1 y1 x y)+ Absolute Quadratic Bezier
//t (x y)+ Shorthand/Smooth Relative Quadratic Bezier
//T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier
//s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve
//S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve
//a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc
//A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc
// return early if data is not defined
if (!data) {
return [];
}
// command string
var cs = data;
// command chars
var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'];
// convert white spaces to commas
cs = cs.replace(new RegExp(' ', 'g'), ',');
// create pipes so that we can split the data
for (var n = 0; n < cc.length; n++) {
cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
}
// create array
var arr = cs.split('|');
var ca = [];
// init context point
var cpx = 0;
var cpy = 0;
for (var n = 1; n < arr.length; n++) {
var str = arr[n];
var c = str.charAt(0);
str = str.slice(1);
// remove ,- for consistency
str = str.replace(new RegExp(',-', 'g'), '-');
// add commas so that it's easy to split
str = str.replace(new RegExp('-', 'g'), ',-');
str = str.replace(new RegExp('e,-', 'g'), 'e-');
var p = str.split(',');
if (p.length > 0 && p[0] === '') {
p.shift();
}
// convert strings to floats
for (var i = 0; i < p.length; i++) {
p[i] = parseFloat(p[i]);
}
while (p.length > 0) {
if (isNaN(p[0])) // case for a trailing comma before next command
break;
var cmd = null;
var points = [];
var startX = cpx,
startY = cpy;
// convert l, H, h, V, and v to L
switch (c) {
// Note: Keep the lineTo's above the moveTo's in this switch
case 'l':
cpx += p.shift();
cpy += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'L':
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
// Note: lineTo handlers need to be above this point
case 'm':
cpx += p.shift();
cpy += p.shift();
cmd = 'M';
points.push(cpx, cpy);
c = 'l';
// subsequent points are treated as relative lineTo
break;
case 'M':
cpx = p.shift();
cpy = p.shift();
cmd = 'M';
points.push(cpx, cpy);
c = 'L';
// subsequent points are treated as absolute lineTo
break;
case 'h':
cpx += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'H':
cpx = p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'v':
cpy += p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'V':
cpy = p.shift();
cmd = 'L';
points.push(cpx, cpy);
break;
case 'C':
points.push(p.shift(), p.shift(), p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
case 'c':
points.push(cpx + p.shift(), cpy + p.shift(), cpx + p.shift(), cpy + p.shift());
cpx += p.shift();
cpy += p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
case 'S':
var ctlPtx = cpx,
ctlPty = cpy;
var prevCmd = ca[ca.length - 1];
if (prevCmd.command === 'C') {
ctlPtx = cpx + (cpx - prevCmd.points[2]);
ctlPty = cpy + (cpy - prevCmd.points[3]);
}
points.push(ctlPtx, ctlPty, p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
case 's':
var ctlPtx = cpx,
ctlPty = cpy;
var prevCmd = ca[ca.length - 1];
if (prevCmd.command === 'C') {
ctlPtx = cpx + (cpx - prevCmd.points[2]);
ctlPty = cpy + (cpy - prevCmd.points[3]);
}
points.push(ctlPtx, ctlPty, cpx + p.shift(), cpy + p.shift());
cpx += p.shift();
cpy += p.shift();
cmd = 'C';
points.push(cpx, cpy);
break;
case 'Q':
points.push(p.shift(), p.shift());
cpx = p.shift();
cpy = p.shift();
points.push(cpx, cpy);
break;
case 'q':
points.push(cpx + p.shift(), cpy + p.shift());
cpx += p.shift();
cpy += p.shift();
cmd = 'Q';
points.push(cpx, cpy);
break;
case 'T':
var ctlPtx = cpx,
ctlPty = cpy;
var prevCmd = ca[ca.length - 1];
if (prevCmd.command === 'Q') {
ctlPtx = cpx + (cpx - prevCmd.points[0]);
ctlPty = cpy + (cpy - prevCmd.points[1]);
}
cpx = p.shift();
cpy = p.shift();
cmd = 'Q';
points.push(ctlPtx, ctlPty, cpx, cpy);
break;
case 't':
var ctlPtx = cpx,
ctlPty = cpy;
var prevCmd = ca[ca.length - 1];
if (prevCmd.command === 'Q') {
ctlPtx = cpx + (cpx - prevCmd.points[0]);
ctlPty = cpy + (cpy - prevCmd.points[1]);
}
cpx += p.shift();
cpy += p.shift();
cmd = 'Q';
points.push(ctlPtx, ctlPty, cpx, cpy);
break;
case 'A':
var rx = p.shift(),
ry = p.shift(),
psi = p.shift(),
fa = p.shift(),
fs = p.shift();
var x1 = cpx,
y1 = cpy;
cpx = p.shift(),
cpy = p.shift();
cmd = 'A';
points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi);
break;
case 'a':
var rx = p.shift(),
ry = p.shift(),
psi = p.shift(),
fa = p.shift(),
fs = p.shift();
var x1 = cpx,
y1 = cpy;
cpx += p.shift(),
cpy += p.shift();
cmd = 'A';
points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi);
break;
}
ca.push({
command : cmd || c,
points : points,
start : {
x : startX,
y : startY
},
pathLength : this.calcLength(startX, startY, cmd || c, points)
});
}
if (c === 'z' || c === 'Z')
ca.push({
command : 'z',
points : [],
start : undefined,
pathLength : 0
});
}
return ca;
},
calcLength : function (x, y, cmd, points) {
var len,
p1,
p2;
switch (cmd) {
case 'L':
return this.getLineLength(x, y, points[0], points[1]);
case 'C':
// Approximates by breaking curve into 100 line segments
len = 0.0;
p1 = this.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
for (t = 0.01; t <= 1; t += 0.01) {
p2 = this.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
p1 = p2;
}
return len;
case 'Q':
// Approximates by breaking curve into 100 line segments
len = 0.0;
p1 = this.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]);
for (t = 0.01; t <= 1; t += 0.01) {
p2 = this.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]);
len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
p1 = p2;
}
return len;
case 'A':
// Approximates by breaking curve into line segments
len = 0.0;
var start = points[4]; // 4 = theta
var dTheta = points[5]; // 5 = dTheta
var end = points[4] + dTheta;
var inc = Math.PI / 180.0; // 1 degree resolution
if (Math.abs(start - end) < inc) {
inc = Math.abs(start - end);
}
// Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi
p1 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0);
if (dTheta < 0) { // clockwise
for (t = start - inc; t > end; t -= inc) {
p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
p1 = p2;
}
} else { // counter-clockwise
for (t = start + inc; t < end; t += inc) {
p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
p1 = p2;
}
}
p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0);
len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
return len;
}
return 0;
},
getLineLength : function (x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
},
getPointOnLine : function (dist, P1x, P1y, P2x, P2y, fromX, fromY) {
if (fromX === undefined) {
fromX = P1x;
}
if (fromY === undefined) {
fromY = P1y;
}
var m = (P2y - P1y) / ((P2x - P1x) + 0.00000001);
var run = Math.sqrt(dist * dist / (1 + m * m));
var rise = m * run;
var pt;
if ((fromY - P1y) / ((fromX - P1x) + 0.00000001) === m) {
pt = {
x : fromX + run,
y : fromY + rise
};
} else {
var ix,
iy;
var len = this.getLineLength(P1x, P1y, P2x, P2y);
if (len < 0.00000001) {
return undefined;
}
var u = (((fromX - P1x) * (P2x - P1x)) + ((fromY - P1y) * (P2y - P1y)));
u = u / (len * len);
ix = P1x + u * (P2x - P1x);
iy = P1y + u * (P2y - P1y);
var pRise = this.getLineLength(fromX, fromY, ix, iy);
var pRun = Math.sqrt(dist * dist - pRise * pRise);
run = Math.sqrt(pRun * pRun / (1 + m * m));
rise = m * run;
pt = {
x : ix + run,
y : iy + rise
};
}
return pt;
},
getPointOnCubicBezier : function (pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) {
function CB1(t) {
return t * t * t;
}
function CB2(t) {
return 3 * t * t * (1 - t);
}
function CB3(t) {
return 3 * t * (1 - t) * (1 - t);
}
function CB4(t) {
return (1 - t) * (1 - t) * (1 - t);
}
var x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct);
var y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct);
return {
x : x,
y : y
};
},
getPointOnQuadraticBezier : function (pct, P1x, P1y, P2x, P2y, P3x, P3y) {
function QB1(t) {
return t * t;
}
function QB2(t) {
return 2 * t * (1 - t);
}
function QB3(t) {
return (1 - t) * (1 - t);
}
var x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct);
var y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct);
return {
x : x,
y : y
};
},
getPointOnEllipticalArc : function (cx, cy, rx, ry, theta, psi) {
var cosPsi = Math.cos(psi),
sinPsi = Math.sin(psi);
var pt = {
x : rx * Math.cos(theta),
y : ry * Math.sin(theta)
};
return {
x : cx + (pt.x * cosPsi - pt.y * sinPsi),
y : cy + (pt.x * sinPsi + pt.y * cosPsi)
};
},
convertEndpointToCenterParameterization : function (x1, y1, x2, y2, fa, fs, rx, ry, psiDeg) {
// Derived from: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
var psi = psiDeg * (Math.PI / 180.0);
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 f = Math.sqrt((((rx * rx) * (ry * ry)) - ((rx * rx) * (yp * yp)) - ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp) + (ry * ry) * (xp * xp)));
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 vMag = function (v) {
return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
};
var vRatio = function (u, v) {
return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v));
};
var vAngle = function (u, v) {
return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v));
};
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 [cx, cy, rx, ry, theta, dTheta, psi, fs];
}
};

View File

@ -21,9 +21,7 @@ Kinetic.Ellipse = Kinetic.Shape.extend({
// call super constructor
this._super(config);
this._convertRadius();
var that = this;
this.on('radiusChange.kinetic', function() {
that._convertRadius();

View File

@ -17,7 +17,6 @@ Kinetic.Sprite = Kinetic.Shape.extend({
config.drawFunc = this.drawFunc;
// call super constructor
this._super(config);
var that = this;
this.on('animationChange.kinetic', function() {
// reset index when animation changes

View File

@ -38,7 +38,6 @@ Kinetic.Text = Kinetic.Shape.extend({
var attr = attrs[n];
this.on(attr + 'Change.kinetic', that._setTextData);
}
that._setTextData();
},
drawFunc: function(context) {

102
src/util/Geometry.js Normal file
View File

@ -0,0 +1,102 @@
/*
* Utility methods written by jfollas to
* handle length and point measurements
*/
Kinetic.Geometry = {
getLineLength: function(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
},
getPointOnLine: function(dist, P1x, P1y, P2x, P2y, fromX, fromY) {
if(fromX === undefined) {
fromX = P1x;
}
if(fromY === undefined) {
fromY = P1y;
}
var m = (P2y - P1y) / ((P2x - P1x) + 0.00000001);
var run = Math.sqrt(dist * dist / (1 + m * m));
var rise = m * run;
var pt;
if((fromY - P1y) / ((fromX - P1x) + 0.00000001) === m) {
pt = {
x: fromX + run,
y: fromY + rise
};
}
else {
var ix, iy;
var len = this.getLineLength(P1x, P1y, P2x, P2y);
if(len < 0.00000001) {
return undefined;
}
var u = (((fromX - P1x) * (P2x - P1x)) + ((fromY - P1y) * (P2y - P1y)));
u = u / (len * len);
ix = P1x + u * (P2x - P1x);
iy = P1y + u * (P2y - P1y);
var pRise = this.getLineLength(fromX, fromY, ix, iy);
var pRun = Math.sqrt(dist * dist - pRise * pRise);
run = Math.sqrt(pRun * pRun / (1 + m * m));
rise = m * run;
pt = {
x: ix + run,
y: iy + rise
};
}
return pt;
},
getPointOnCubicBezier: function(pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) {
function CB1(t) {
return t * t * t;
}
function CB2(t) {
return 3 * t * t * (1 - t);
}
function CB3(t) {
return 3 * t * (1 - t) * (1 - t);
}
function CB4(t) {
return (1 - t) * (1 - t) * (1 - t);
}
var x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct);
var y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct);
return {
x: x,
y: y
};
},
getPointOnQuadraticBezier: function(pct, P1x, P1y, P2x, P2y, P3x, P3y) {
function QB1(t) {
return t * t;
}
function QB2(t) {
return 2 * t * (1 - t);
}
function QB3(t) {
return (1 - t) * (1 - t);
}
var x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct);
var y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct);
return {
x: x,
y: y
};
},
getPointOnEllipticalArc: function(cx, cy, rx, ry, theta, psi) {
var cosPsi = Math.cos(psi), sinPsi = Math.sin(psi);
var pt = {
x: rx * Math.cos(theta),
y: ry * Math.sin(theta)
};
return {
x: cx + (pt.x * cosPsi - pt.y * sinPsi),
y: cy + (pt.x * sinPsi + pt.y * cosPsi)
};
}
};

View File

@ -11,6 +11,7 @@
<!-- assets -->
<script src="../js/Test.js"></script>
<script src="../js/performanceTests.js"></script>
<script src="../assets/worldMap.js"></script>
<script>
window.onload = function() {
var test = new Test();

View File

@ -22,8 +22,8 @@ Test.prototype.tests = {
layer.add(rect);
}
stage.add(layer);
console.profileEnd();
console.profileEnd();
endTimer('add and draw 1,000 Kinetic rectangles');
},
@ -59,9 +59,9 @@ Test.prototype.tests = {
});
stage.start();
setTimeout(function() {
//stage.stop();
//stage.stop();
}, 1000)
},
'DRAWING - draw rect vs image from image data': function(containerId) {
@ -219,5 +219,53 @@ Test.prototype.tests = {
endTimer('draw 1,000 cached stars');
}
});
},
'*PATH - add map path': function(containerId) {
startTimer();
var stage = new Kinetic.Stage({
container: containerId,
width: 1024,
height: 480,
throttle: 80,
scale: 0.5,
x: 50,
y: 10
});
var mapLayer = new Kinetic.Layer();
for(var key in worldMap.shapes) {
var c = worldMap.shapes[key];
var path = new Kinetic.Plugins.Path({
data: c,
fill: '#ccc',
stroke: '#999',
strokeWidth: 1
});
if(key === 'US')
test(path.dataArray[0].command === 'M', 'first command should be a moveTo');
path.on('mouseover', function() {
this.setFill('red');
mapLayer.draw();
});
path.on('mouseout', function() {
this.setFill('#ccc');
mapLayer.draw();
});
mapLayer.add(path);
}
stage.add(mapLayer);
endTimer('time build and to draw map');
mapLayer.beforeDraw(startTimer);
mapLayer.afterDraw(function() {
endTimer('redraw layer');
});
}
};

File diff suppressed because one or more lines are too long