Merge pull request #24 from jfollas/master

SVG Path additions
This commit is contained in:
ericdrowell 2012-05-29 21:21:16 -07:00
commit 2bc0465186
2 changed files with 408 additions and 49 deletions

View File

@ -27,6 +27,12 @@ Kinetic.Path = function(config) {
case 'M': case 'M':
context.moveTo(p[0], p[1]); context.moveTo(p[0], p[1]);
break; break;
case 'C':
context.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]);
break;
case 'Q':
context.quadraticCurveTo(p[0], p[1], p[2], p[3]);
break;
case 'z': case 'z':
context.closePath(); context.closePath();
break; break;
@ -51,10 +57,36 @@ Kinetic.Path.prototype = {
* rendering * rendering
*/ */
getCommandsArray: function() { getCommandsArray: function() {
// 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
// Note: SVG a,A not implemented here
//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
// command string // command string
var cs = this.attrs.commands; var cs = this.attrs.commands;
// command chars // command chars
var cc = ['M', 'l', 'L', 'v', 'V', 'h', 'H', 'z']; var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S'];
// convert white spaces to commas // convert white spaces to commas
cs = cs.replace(new RegExp(' ', 'g'), ','); cs = cs.replace(new RegExp(' ', 'g'), ',');
// create pipes so that we can split the commands // create pipes so that we can split the commands
@ -83,44 +115,157 @@ Kinetic.Path.prototype = {
for(var i = 0; i < p.length; i++) { for(var i = 0; i < p.length; i++) {
p[i] = parseFloat(p[i]); p[i] = parseFloat(p[i]);
} }
// convert l, H, h, V, and v to L
switch(c) { while (p.length > 0)
case 'M': {
cpx = p[0]; if (isNaN(p[0])) // case for a trailing comma before next command
cpy = p[1]; break;
break;
case 'l': var cmd = undefined;
cpx += p[0]; var points = [];
cpy += p[1];
break; // convert l, H, h, V, and v to L
case 'L': switch(c) {
cpx = p[0];
cpy = p[1]; // Note: Keep the lineTo's above the moveTo's in this switch
break; case 'l':
case 'h': cpx += p.shift();
cpx += p[0]; cpy += p.shift();
break; cmd = 'L';
case 'H': points.push(cpx, cpy);
cpx = p[0]; break;
break; case 'L':
case 'v': cpx = p.shift();
cpy += p[0]; cpy = p.shift();
break; points.push(cpx, cpy);
case 'V': break;
cpy = p[0];
break; // Note: lineTo handlers need to be above this point
} case 'm':
// reassign command cpx += p.shift();
if(c == 'l' || c == 'V' || c == 'v' || c == 'H' || c == 'h') { cpy += p.shift();
c = 'L'; cmd = 'M';
p[0] = cpx; points.push(cpx, cpy);
p[1] = cpy; c = 'l'; // subsequent points are treated as relative lineTo
} break;
ca.push({ case 'M':
command: c, cpx = p.shift();
points: p 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;
}
ca.push({
command: cmd || c,
points: points
});
}
if (c === 'z' || c === 'Z')
ca.push( {command: 'z', points: [] });
}
return ca; return ca;
}, },
/** /**
@ -133,7 +278,7 @@ Kinetic.Path.prototype = {
* set SVG path commands string. This method * set SVG path commands string. This method
* also automatically parses the commands string * also automatically parses the commands string
* into a commands array. Currently supported SVG commands: * into a commands array. Currently supported SVG commands:
* M, L, l, H, h, V, v, z * M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, Z, z
* @param {String} SVG path command string * @param {String} SVG path command string
*/ */
setCommands: function(commands) { setCommands: function(commands) {

View File

@ -1227,6 +1227,50 @@ Test.prototype.tests = {
path.setCommands('M200,100h100v50z'); path.setCommands('M200,100h100v50z');
}, },
'SHAPE - moveTo with implied lineTos and trailing comma': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 1024,
height: 480,
scale: 0.5,
x: 50,
y: 10
});
var layer = new Kinetic.Layer();
var path = new Kinetic.Path({
commands: 'm200,100,100,0,0,50,z',
fill: '#fcc',
stroke: '#333',
strokeWidth: 2,
shadow: {
color: 'maroon',
blur: 2,
offset: [10, 10],
alpha: 0.5
},
draggable: true
});
path.on('mouseover', function() {
this.setFill('red');
layer.draw();
});
path.on('mouseout', function() {
this.setFill('#ccc');
layer.draw();
});
layer.add(path);
stage.add(layer);
test(path.getCommands() === 'm200,100,100,0,0,50,z', 'commands are incorrect');
test(path.getCommandsArray().length === 4, 'commands array should have 4 elements');
test(path.getCommandsArray()[1].command === 'L', 'second command should be an implied lineTo');
},
'SHAPE - add map path': function(containerId) { 'SHAPE - add map path': function(containerId) {
var stage = new Kinetic.Stage({ var stage = new Kinetic.Stage({
container: containerId, container: containerId,
@ -1247,17 +1291,12 @@ Test.prototype.tests = {
fill: '#ccc', fill: '#ccc',
stroke: '#999', stroke: '#999',
strokeWidth: 1, strokeWidth: 1,
/*
shadow: {
color: 'black',
blur: 2,
offset: [10, 10]
}
*/
}); });
if (key === 'US')
test(path.getCommandsArray()[0].command === 'M', 'first command should be a moveTo');
path.on('mouseover', function() { path.on('mouseover', function() {
//console.log(1)
this.setFill('red'); this.setFill('red');
mapLayer.draw(); mapLayer.draw();
}); });
@ -1273,6 +1312,181 @@ Test.prototype.tests = {
stage.add(mapLayer); stage.add(mapLayer);
}, },
'SHAPE - curved arrow path': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 1024,
height: 480,
throttle: 80,
scale: 1.5,
x: 50,
y: 10
});
var layer = new Kinetic.Layer();
var c = "M12.582,9.551C3.251,16.237,0.921,29.021,7.08,38.564l-2.36,1.689l4.893,2.262l4.893,2.262l-0.568-5.36l-0.567-5.359l-2.365,1.694c-4.657-7.375-2.83-17.185,4.352-22.33c7.451-5.338,17.817-3.625,23.156,3.824c5.337,7.449,3.625,17.813-3.821,23.152l2.857,3.988c9.617-6.893,11.827-20.277,4.935-29.896C35.591,4.87,22.204,2.658,12.582,9.551z";
var path = new Kinetic.Path({
commands: c,
fill: '#ccc',
stroke: '#999',
strokeWidth: 1,
});
path.on('mouseover', function() {
this.setFill('red');
layer.draw();
});
path.on('mouseout', function() {
this.setFill('#ccc');
layer.draw();
});
layer.add(path);
stage.add(layer);
},
'SHAPE - Quadradic Curve test from SVG w3c spec': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 1024,
height: 480,
throttle: 80,
scale: 0.25,
x: 50,
y: 10
});
var layer = new Kinetic.Layer();
var c = "M200,300 Q400,50 600,300 T1000,300";
var path = new Kinetic.Path({
commands: c,
stroke: 'red',
strokeWidth: 5,
});
layer.add(path);
layer.add(new Kinetic.Circle({
x: 200,
y: 300,
radius: 10,
fill: 'black'
}));
layer.add(new Kinetic.Circle({
x: 600,
y: 300,
radius: 10,
fill: 'black'
}));
layer.add(new Kinetic.Circle({
x: 1000,
y: 300,
radius: 10,
fill: 'black'
}));
layer.add(new Kinetic.Circle({
x: 400,
y: 50,
radius: 10,
fill: '#888'
}));
layer.add(new Kinetic.Circle({
x: 800,
y: 550,
radius: 10,
fill: '#888'
}));
layer.add(new Kinetic.Path({
commands: "M200,300 L400,50L600,300L800,550L1000,300",
stroke: "#888",
strokeWidth: 2
}));
stage.add(layer);
},
'SHAPE - Cubic Bezier Curve test from SVG w3c spec': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 1024,
height: 480,
throttle: 80,
scale: 0.5,
x: 50,
y: 10
});
var layer = new Kinetic.Layer();
var c = "M100,200 C100,100 250,100 250,200 S400,300 400,200";
var path = new Kinetic.Path({
commands: c,
stroke: 'red',
strokeWidth: 5,
});
layer.add(path);
layer.add(new Kinetic.Circle({
x: 100,
y: 200,
radius: 10,
stroke: '#888'
}));
layer.add(new Kinetic.Circle({
x: 250,
y: 200,
radius: 10,
stroke: '#888'
}));
layer.add(new Kinetic.Circle({
x: 400,
y: 200,
radius: 10,
stroke: '#888'
}));
layer.add(new Kinetic.Circle({
x: 100,
y: 100,
radius: 10,
fill: '#888'
}));
layer.add(new Kinetic.Circle({
x: 250,
y: 100,
radius: 10,
fill: '#888'
}));
layer.add(new Kinetic.Circle({
x: 400,
y: 300,
radius: 10,
fill: '#888'
}));
layer.add(new Kinetic.Circle({
x: 250,
y: 300,
radius: 10,
stroke: 'blue'
}));
stage.add(layer);
},
'SHAPE - add shape with custom attr pointing to self': function(containerId) { 'SHAPE - add shape with custom attr pointing to self': function(containerId) {
var stage = new Kinetic.Stage({ var stage = new Kinetic.Stage({
container: containerId, container: containerId,