Add support for elliptical arc commands (a, A) in SVG paths. Thanks @devongovett for initial code (and algorithm). Add 4 different arc test files (can be loaded in kitchensink demo).

This commit is contained in:
kangax 2011-07-05 01:28:03 -04:00
parent 5fc29b1dbb
commit e3c311dfb5
8 changed files with 317 additions and 17 deletions

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="200" height="200" viewBox="0 0 200 200">
<path d="M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120" stroke="red" stroke-width="4" fill="none" />
</svg>

After

Width:  |  Height:  |  Size: 463 B

View file

@ -0,0 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="320" height="320"
xmlns="http://www.w3.org/2000/svg" version="1.1">
<path d="M160,160 h-150 a150,150 0 1,0 150,-150 z" fill="red" stroke="blue" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg viewBox = "0 0 1100 400" version = "1.1">
<g stroke = "black" stroke-width = "3" fill = "none">
<path d = "M 50 200 a 100 50 0 1 1 250 50"/>
<path d = "M 400 100 a 100 50 30 1 1 250 50"/>
<path d = "M 400 300 a 100 50 45 1 1 250 50"/>
<path d = "M 750 200 a 100 50 135 1 1 250 50"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View file

@ -0,0 +1,7 @@
<?xml version="1.0" standalone="no"?>
<svg width="325px" height="325px" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z" fill="green"/>
<path d="M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z" fill="red"/>
<path d="M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z" fill="purple"/>
<path d="M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z" fill="blue"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

View file

@ -80,10 +80,7 @@
<li><button class="shape" id="shape85"><strong>xxx</strong> paths</button></li>
<li><button class="shape" id="shape86"><strong>xxx</strong> paths</button></li>
<li><button class="shape" id="shape87"><strong>xxx</strong> paths</button></li> -->
<li><button class="shape" id="shape54">Image (<strong>1</strong> path)</button></li>
<li><button class="shape" id="shape74">Gradient (<strong>1</strong> path)</button></li>
<li><button class="shape" id="shape66">Gradient (<strong>1</strong> path)</button></li>
<li><button class="shape" id="shape75">Gradient (<strong>1</strong> path)</button></li>
<li><button class="shape" id="shape25"><strong>36</strong> paths</button></li>
<li><button class="shape" id="shape36"><strong>41</strong> paths</button></li>
<li><button class="shape" id="shape58"><strong>54</strong> paths</button></li>
@ -153,8 +150,17 @@
<li><button class="shape" id="shape44"><strong>22375</strong> paths</button></li>
<li><button class="shape" id="shape72"><strong>29303</strong> paths</button></li>
<li><button class="shape" id="shape48"><strong>41787</strong> paths</button></li>
<li><button class="shape" id="shape103"><strong>xxx</strong> paths</button></li>
<!-- <li><button class="shape" id="shape100"><strong>x</strong> paths</button></li> -->
<li><button class="shape" id="shape54">Image</button></li>
<li><button class="shape" id="shape74">Gradient 1</button></li>
<li><button class="shape" id="shape66">Gradient 2</button></li>
<li><button class="shape" id="shape75">Gradient 3</button></li>
<li><button class="shape" id="shape104">Arc(s) 1</button></li>
<li><button class="shape" id="shape105">Arc(s) 2</button></li>
<li><button class="shape" id="shape106">Arc(s) 3</button></li>
<li><button class="shape" id="shape107">Arc(s) 4</button></li>
<!-- <li><button class="shape" id="shape103"><strong>xxx</strong> paths</button></li> -->
</ul>
<ul>

140
dist/all.js vendored
View file

@ -8434,6 +8434,111 @@ fabric.util.animate = animate;
(function(global) {
function drawArc(ctx, x, y, coords) {
var rx = coords[0];
var ry = coords[1];
var rot = coords[2];
var large = coords[3];
var sweep = coords[4];
var ex = coords[5];
var ey = coords[6];
var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y);
for (var i=0; i<segs.length; i++) {
var bez = segmentToBezier.apply(this, segs[i]);
ctx.bezierCurveTo.apply(ctx, bez);
}
}
var arcToSegmentsCache = { },
segmentToBezierCache = { },
_join = Array.prototype.join,
argsString;
function arcToSegments(x, y, rx, ry, large, sweep, rotateX, ox, oy) {
argsString = _join.call(arguments);
if (arcToSegmentsCache[argsString]) {
return arcToSegmentsCache[argsString];
}
var th = rotateX * (Math.PI/180);
var sin_th = Math.sin(th);
var cos_th = Math.cos(th);
rx = Math.abs(rx);
ry = Math.abs(ry);
var px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5;
var py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5;
var pl = (px*px) / (rx*rx) + (py*py) / (ry*ry);
if (pl > 1) {
pl = Math.sqrt(pl);
rx *= pl;
ry *= pl;
}
var a00 = cos_th / rx;
var a01 = sin_th / rx;
var a10 = (-sin_th) / ry;
var a11 = (cos_th) / ry;
var x0 = a00 * ox + a01 * oy;
var y0 = a10 * ox + a11 * oy;
var x1 = a00 * x + a01 * y;
var y1 = a10 * x + a11 * y;
var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0);
var sfactor_sq = 1 / d - 0.25;
if (sfactor_sq < 0) sfactor_sq = 0;
var sfactor = Math.sqrt(sfactor_sq);
if (sweep == large) sfactor = -sfactor;
var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0);
var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0);
var th0 = Math.atan2(y0-yc, x0-xc);
var th1 = Math.atan2(y1-yc, x1-xc);
var th_arc = th1-th0;
if (th_arc < 0 && sweep == 1){
th_arc += 2*Math.PI;
} else if (th_arc > 0 && sweep == 0) {
th_arc -= 2 * Math.PI;
}
var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001)));
var result = [];
for (var i=0; i<segments; i++) {
var th2 = th0 + i * th_arc / segments;
var th3 = th0 + (i+1) * th_arc / segments;
result[i] = [xc, yc, th2, th3, rx, ry, sin_th, cos_th];
}
return (arcToSegmentsCache[argsString] = result);
}
function segmentToBezier(cx, cy, th0, th1, rx, ry, sin_th, cos_th) {
argsString = _join.call(arguments);
if (segmentToBezierCache[argsString]) {
return segmentToBezierCache[argsString];
}
var a00 = cos_th * rx;
var a01 = -sin_th * ry;
var a10 = sin_th * rx;
var a11 = cos_th * ry;
var th_half = 0.5 * (th1 - th0);
var t = (8/3) * Math.sin(th_half * 0.5) * Math.sin(th_half * 0.5) / Math.sin(th_half);
var x1 = cx + Math.cos(th0) - t * Math.sin(th0);
var y1 = cy + Math.sin(th0) + t * Math.cos(th0);
var x3 = cx + Math.cos(th1);
var y3 = cy + Math.sin(th1);
var x2 = x3 + t * Math.sin(th1);
var y2 = y3 - t * Math.cos(th1);
return (segmentToBezierCache[argsString] = [
a00 * x1 + a01 * y1, a10 * x1 + a11 * y1,
a00 * x2 + a01 * y2, a10 * x2 + a11 * y2,
a00 * x3 + a01 * y3, a10 * x3 + a11 * y3
]);
}
"use strict";
var fabric = global.fabric || (global.fabric = { }),
@ -8707,9 +8812,31 @@ fabric.util.animate = animate;
break;
case 'a':
drawArc(ctx, x + l, y + t, [
current[1],
current[2],
current[3],
current[4],
current[5],
current[6] + x + l,
current[7] + y + t
]);
x += current[6];
y += current[7];
break;
case 'A':
drawArc(ctx, x + l, y + t, [
current[1],
current[2],
current[3],
current[4],
current[5],
current[6] + l,
current[7] + t
]);
x = current[6];
y = current[7];
break;
case 'z':
@ -8824,17 +8951,22 @@ fabric.util.animate = animate;
_parsePath: function() {
var result = [ ],
currentPath,
chunks;
chunks,
parsed;
for (var i = 0, j, chunksParsed, len = this.path.length; i < len; i++) {
currentPath = this.path[i];
chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/);
j = chunks.length, chunksParsed = [ currentPath.charAt(0) ];
while (j--) {
chunksParsed[j+1] = parseFloat(chunks[j]);
chunksParsed = [ currentPath.charAt(0) ];
for (var j = 0, jlen = chunks.length; j < jlen; j++) {
parsed = parseFloat(chunks[j]);
if (!isNaN(parsed)) {
chunksParsed.push(parsed);
}
}
result.push(chunksParsed);
}
return result;
},

View file

@ -1,6 +1,6 @@
/*! Fabric.js Copyright 2008-2011, Bitsonnet (Juriy Zaytsev, Maxim Chernyak) */
var fabric = fabric || { version: "0.3.2" };
var fabric = fabric || { version: "0.3.3" };
/**
* Wrapper around `console.log` (when available)

View file

@ -2,6 +2,112 @@
(function(global) {
function drawArc(ctx, x, y, coords) {
var rx = coords[0];
var ry = coords[1];
var rot = coords[2];
var large = coords[3];
var sweep = coords[4];
var ex = coords[5];
var ey = coords[6];
var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y);
for (var i=0; i<segs.length; i++) {
var bez = segmentToBezier.apply(this, segs[i]);
ctx.bezierCurveTo.apply(ctx, bez);
}
}
var arcToSegmentsCache = { },
segmentToBezierCache = { },
_join = Array.prototype.join,
argsString;
// Copied from Inkscape svgtopdf, thanks!
function arcToSegments(x, y, rx, ry, large, sweep, rotateX, ox, oy) {
argsString = _join.call(arguments);
if (arcToSegmentsCache[argsString]) {
return arcToSegmentsCache[argsString];
}
var th = rotateX * (Math.PI/180);
var sin_th = Math.sin(th);
var cos_th = Math.cos(th);
rx = Math.abs(rx);
ry = Math.abs(ry);
var px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5;
var py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5;
var pl = (px*px) / (rx*rx) + (py*py) / (ry*ry);
if (pl > 1) {
pl = Math.sqrt(pl);
rx *= pl;
ry *= pl;
}
var a00 = cos_th / rx;
var a01 = sin_th / rx;
var a10 = (-sin_th) / ry;
var a11 = (cos_th) / ry;
var x0 = a00 * ox + a01 * oy;
var y0 = a10 * ox + a11 * oy;
var x1 = a00 * x + a01 * y;
var y1 = a10 * x + a11 * y;
var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0);
var sfactor_sq = 1 / d - 0.25;
if (sfactor_sq < 0) sfactor_sq = 0;
var sfactor = Math.sqrt(sfactor_sq);
if (sweep == large) sfactor = -sfactor;
var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0);
var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0);
var th0 = Math.atan2(y0-yc, x0-xc);
var th1 = Math.atan2(y1-yc, x1-xc);
var th_arc = th1-th0;
if (th_arc < 0 && sweep == 1){
th_arc += 2*Math.PI;
} else if (th_arc > 0 && sweep == 0) {
th_arc -= 2 * Math.PI;
}
var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001)));
var result = [];
for (var i=0; i<segments; i++) {
var th2 = th0 + i * th_arc / segments;
var th3 = th0 + (i+1) * th_arc / segments;
result[i] = [xc, yc, th2, th3, rx, ry, sin_th, cos_th];
}
return (arcToSegmentsCache[argsString] = result);
}
function segmentToBezier(cx, cy, th0, th1, rx, ry, sin_th, cos_th) {
argsString = _join.call(arguments);
if (segmentToBezierCache[argsString]) {
return segmentToBezierCache[argsString];
}
var a00 = cos_th * rx;
var a01 = -sin_th * ry;
var a10 = sin_th * rx;
var a11 = cos_th * ry;
var th_half = 0.5 * (th1 - th0);
var t = (8/3) * Math.sin(th_half * 0.5) * Math.sin(th_half * 0.5) / Math.sin(th_half);
var x1 = cx + Math.cos(th0) - t * Math.sin(th0);
var y1 = cy + Math.sin(th0) + t * Math.cos(th0);
var x3 = cx + Math.cos(th1);
var y3 = cy + Math.sin(th1);
var x2 = x3 + t * Math.sin(th1);
var y2 = y3 - t * Math.cos(th1);
return (segmentToBezierCache[argsString] = [
a00 * x1 + a01 * y1, a10 * x1 + a11 * y1,
a00 * x2 + a01 * y2, a10 * x2 + a11 * y2,
a00 * x3 + a01 * y3, a10 * x3 + a11 * y3
]);
}
"use strict";
var fabric = global.fabric || (global.fabric = { }),
@ -280,11 +386,33 @@
break;
case 'a':
// TODO (kangax): implement arc (relative)
// TODO: optimize this
drawArc(ctx, x + l, y + t, [
current[1],
current[2],
current[3],
current[4],
current[5],
current[6] + x + l,
current[7] + y + t
]);
x += current[6];
y += current[7];
break;
case 'A':
// TODO (kangax): implement arc (absolute)
// TODO: optimize this
drawArc(ctx, x + l, y + t, [
current[1],
current[2],
current[3],
current[4],
current[5],
current[6] + l,
current[7] + t
]);
x = current[6];
y = current[7];
break;
case 'z':
@ -400,19 +528,24 @@
_parsePath: function() {
var result = [ ],
currentPath,
chunks;
chunks,
parsed;
// use plain loop for performance reasons.
// this chunk of code can be called thousands of times per second (when parsing large shapes)
for (var i = 0, j, chunksParsed, len = this.path.length; i < len; i++) {
currentPath = this.path[i];
chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/);
j = chunks.length, chunksParsed = [ currentPath.charAt(0) ];
while (j--) {
chunksParsed[j+1] = parseFloat(chunks[j]);
chunksParsed = [ currentPath.charAt(0) ];
for (var j = 0, jlen = chunks.length; j < jlen; j++) {
parsed = parseFloat(chunks[j]);
if (!isNaN(parsed)) {
chunksParsed.push(parsed);
}
}
result.push(chunksParsed);
}
return result;
},