From d0acfb14e1bb238e8f6926c3654bb3d992be06f4 Mon Sep 17 00:00:00 2001 From: Jason Follas Date: Tue, 29 May 2012 13:49:11 -0400 Subject: [PATCH 1/4] Added multi-point string handling to the path parser. Added support for 'm' and 'Z'. --- src/shapes/Path.js | 125 +++++++++++++++++++++++++++++------------- tests/js/unitTests.js | 44 +++++++++++++++ 2 files changed, 130 insertions(+), 39 deletions(-) diff --git a/src/shapes/Path.js b/src/shapes/Path.js index 14281902..90856d45 100644 --- a/src/shapes/Path.js +++ b/src/shapes/Path.js @@ -26,7 +26,11 @@ Kinetic.Path = function(config) { break; case 'M': context.moveTo(p[0], p[1]); + c = 'L'; // Subsequent points are treated as lineTo break; + //case 'C': + // context.bezierCurveTo(p[0], p[1], p[2], p[3], path[i].p.x, path[i].p.y); + case 'z': context.closePath(); break; @@ -51,10 +55,29 @@ Kinetic.Path.prototype = { * rendering */ 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 + // Note: SVG s,S,t,T,a,A not implemented here + + // command string var cs = this.attrs.commands; // command chars - var cc = ['M', 'l', 'L', 'v', 'V', 'h', 'H', 'z']; + var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z']; // convert white spaces to commas cs = cs.replace(new RegExp(' ', 'g'), ','); // create pipes so that we can split the commands @@ -83,44 +106,68 @@ Kinetic.Path.prototype = { for(var i = 0; i < p.length; i++) { p[i] = parseFloat(p[i]); } - // convert l, H, h, V, and v to L - switch(c) { - case 'M': - cpx = p[0]; - cpy = p[1]; - break; - case 'l': - cpx += p[0]; - cpy += p[1]; - break; - case 'L': - cpx = p[0]; - cpy = p[1]; - break; - case 'h': - cpx += p[0]; - break; - case 'H': - cpx = p[0]; - break; - case 'v': - cpy += p[0]; - break; - case 'V': - cpy = p[0]; - break; - } - // reassign command - if(c == 'l' || c == 'V' || c == 'v' || c == 'H' || c == 'h') { - c = 'L'; - p[0] = cpx; - p[1] = cpy; - } - ca.push({ - command: c, - points: p - }); - } + + while (p.length > 0) + { + if (isNaN(p[0])) // case for a trailing comma before next command + break; + + var cmd = undefined; + + // convert l, H, h, V, and v to L + switch(c) { + case 'm': + cmd = 'M'; + cpx += p.shift(); + cpy += p.shift(); + c = 'l'; // subsequent points are treated as relative lineTo + break; + case 'M': + cmd = 'M'; + cpx = p.shift(); + cpy = p.shift(); + c = 'L'; // subsequent points are treated as absolute lineTo + break; + case 'l': + cmd = 'L'; + cpx += p.shift(); + cpy += p.shift(); + break; + case 'L': + cmd = 'L'; + cpx = p.shift(); + cpy = p.shift(); + break; + case 'h': + cmd = 'L'; + cpx += p.shift(); + break; + case 'H': + cmd = 'L'; + cpx = p.shift(); + break; + case 'v': + cmd = 'L'; + cpy += p.shift(); + break; + case 'V': + cmd = 'L'; + cpy = p.shift(); + break; + } + + ca.push({ + command: cmd || c, + points: [cpx, cpy] // Need to add additional points if curves, etc. + }); + + } + + if (c === 'z' || c === 'Z') + ca.push( {command: 'z', points: [] }); + } + + return ca; }, /** diff --git a/tests/js/unitTests.js b/tests/js/unitTests.js index f50a3fb0..c6f4e9f6 100644 --- a/tests/js/unitTests.js +++ b/tests/js/unitTests.js @@ -1226,6 +1226,50 @@ Test.prototype.tests = { 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) { var stage = new Kinetic.Stage({ From 90c07bdaa5cbb2b95f35fec99885be8793458960 Mon Sep 17 00:00:00 2001 From: Jason Follas Date: Tue, 29 May 2012 14:41:23 -0400 Subject: [PATCH 2/4] Implemented 'c', 'C', 'q', 'Q' paths --- src/shapes/Path.js | 88 ++++++++++++++++++++++++++++++++----------- tests/js/unitTests.js | 54 ++++++++++++++++++++------ 2 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/shapes/Path.js b/src/shapes/Path.js index 90856d45..1d0e3bd2 100644 --- a/src/shapes/Path.js +++ b/src/shapes/Path.js @@ -26,11 +26,13 @@ Kinetic.Path = function(config) { break; case 'M': context.moveTo(p[0], p[1]); - c = 'L'; // Subsequent points are treated as lineTo break; - //case 'C': - // context.bezierCurveTo(p[0], p[1], p[2], p[3], path[i].p.x, path[i].p.y); - + 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': context.closePath(); break; @@ -71,13 +73,14 @@ Kinetic.Path.prototype = { //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 + // Note: SVG s,S,t,T,a,A not implemented here // command string var cs = this.attrs.commands; // command chars - var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z']; + var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q']; // convert white spaces to commas cs = cs.replace(new RegExp(' ', 'g'), ','); // create pipes so that we can split the commands @@ -113,52 +116,91 @@ Kinetic.Path.prototype = { break; var cmd = undefined; + var points = []; // convert l, H, h, V, and v to L switch(c) { - case 'm': - cmd = 'M'; + + // 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': - cmd = 'M'; cpx = p.shift(); cpy = p.shift(); + cmd = 'M'; + points.push(cpx, cpy); c = 'L'; // subsequent points are treated as absolute lineTo break; - case 'l': - cmd = 'L'; - cpx += p.shift(); - cpy += p.shift(); - break; - case 'L': - cmd = 'L'; - cpx = p.shift(); - cpy = p.shift(); - break; + case 'h': - cmd = 'L'; cpx += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); break; case 'H': - cmd = 'L'; cpx = p.shift(); + cmd = 'L'; + points.push(cpx, cpy); break; case 'v': - cmd = 'L'; cpy += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); break; case 'V': - cmd = 'L'; 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 '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; } ca.push({ command: cmd || c, - points: [cpx, cpy] // Need to add additional points if curves, etc. + points: points }); } diff --git a/tests/js/unitTests.js b/tests/js/unitTests.js index c6f4e9f6..a8c2cfa1 100644 --- a/tests/js/unitTests.js +++ b/tests/js/unitTests.js @@ -1227,7 +1227,7 @@ Test.prototype.tests = { path.setCommands('M200,100h100v50z'); }, - 'SHAPE - moveTo with implied lineTos and trailing comma': function(containerId) { + 'SHAPE - moveTo with implied lineTos and trailing comma': function(containerId) { var stage = new Kinetic.Stage({ container: containerId, width: 1024, @@ -1268,8 +1268,8 @@ Test.prototype.tests = { 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'); + + test(path.getCommandsArray()[1].command === 'L', 'second command should be an implied lineTo'); }, 'SHAPE - add map path': function(containerId) { var stage = new Kinetic.Stage({ @@ -1291,17 +1291,12 @@ Test.prototype.tests = { fill: '#ccc', stroke: '#999', 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() { - //console.log(1) this.setFill('red'); mapLayer.draw(); }); @@ -1317,6 +1312,41 @@ Test.prototype.tests = { 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 - add shape with custom attr pointing to self': function(containerId) { var stage = new Kinetic.Stage({ container: containerId, From 90364408d70ead1c4a2d1b7881cb4081e93a6a21 Mon Sep 17 00:00:00 2001 From: Jason Follas Date: Tue, 29 May 2012 15:22:06 -0400 Subject: [PATCH 3/4] implemented 'T' and 't'. --- src/shapes/Path.js | 29 ++++++++++++++++++- tests/js/unitTests.js | 66 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/shapes/Path.js b/src/shapes/Path.js index 1d0e3bd2..ef01503c 100644 --- a/src/shapes/Path.js +++ b/src/shapes/Path.js @@ -74,13 +74,16 @@ Kinetic.Path.prototype = { //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 + // Note: SVG s,S,t,T,a,A not implemented here // command string var cs = this.attrs.commands; // command chars - var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q']; + var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q', 't', 'T']; // convert white spaces to commas cs = cs.replace(new RegExp(' ', 'g'), ','); // create pipes so that we can split the commands @@ -196,6 +199,30 @@ Kinetic.Path.prototype = { 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({ diff --git a/tests/js/unitTests.js b/tests/js/unitTests.js index a8c2cfa1..7b606ce2 100644 --- a/tests/js/unitTests.js +++ b/tests/js/unitTests.js @@ -1346,6 +1346,72 @@ Test.prototype.tests = { 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 - add shape with custom attr pointing to self': function(containerId) { var stage = new Kinetic.Stage({ From 59ee010050cee9f01c081432afd375dda417648c Mon Sep 17 00:00:00 2001 From: Jason Follas Date: Tue, 29 May 2012 16:14:58 -0400 Subject: [PATCH 4/4] Implemented 's' and 'S' --- src/shapes/Path.js | 45 +++++++++++++++++++++----- tests/js/unitTests.js | 74 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 8 deletions(-) diff --git a/src/shapes/Path.js b/src/shapes/Path.js index ef01503c..29ac5ac2 100644 --- a/src/shapes/Path.js +++ b/src/shapes/Path.js @@ -73,17 +73,20 @@ Kinetic.Path.prototype = { //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 s,S,t,T,a,A not implemented here + // 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 var cs = this.attrs.commands; // command chars - var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q', 't', 'T']; + 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 cs = cs.replace(new RegExp(' ', 'g'), ','); // create pipes so that we can split the commands @@ -185,7 +188,33 @@ Kinetic.Path.prototype = { cpy += p.shift(); cmd = 'C' points.push(cpx, cpy); - break; + 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(); @@ -222,7 +251,8 @@ Kinetic.Path.prototype = { cpy += p.shift(); cmd = 'Q'; points.push(ctlPtx, ctlPty, cpx, cpy); - break; + break; + } ca.push({ @@ -235,8 +265,7 @@ Kinetic.Path.prototype = { if (c === 'z' || c === 'Z') ca.push( {command: 'z', points: [] }); } - - + return ca; }, /** @@ -249,7 +278,7 @@ Kinetic.Path.prototype = { * set SVG path commands string. This method * also automatically parses the commands string * 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 */ setCommands: function(commands) { diff --git a/tests/js/unitTests.js b/tests/js/unitTests.js index 7b606ce2..bbaed682 100644 --- a/tests/js/unitTests.js +++ b/tests/js/unitTests.js @@ -1412,6 +1412,80 @@ Test.prototype.tests = { 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) { var stage = new Kinetic.Stage({