diff --git a/src/shapes/Path.js b/src/shapes/Path.js index 14281902..29ac5ac2 100644 --- a/src/shapes/Path.js +++ b/src/shapes/Path.js @@ -27,6 +27,12 @@ Kinetic.Path = function(config) { case 'M': context.moveTo(p[0], p[1]); 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': context.closePath(); break; @@ -51,10 +57,36 @@ 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 + //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 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', '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 @@ -83,44 +115,157 @@ 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; + var points = []; + + // 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; + + } + + ca.push({ + command: cmd || c, + points: points + }); + + } + + if (c === 'z' || c === 'Z') + ca.push( {command: 'z', points: [] }); + } + return ca; }, /** @@ -133,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 f50a3fb0..bbaed682 100644 --- a/tests/js/unitTests.js +++ b/tests/js/unitTests.js @@ -1227,6 +1227,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({ container: containerId, @@ -1247,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(); }); @@ -1273,6 +1312,181 @@ 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 - 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) { var stage = new Kinetic.Stage({ container: containerId,