Last active
October 3, 2018 05:40
-
-
Save dustMason/03c8f136eb19eacda751ead8c6ab1fea to your computer and use it in GitHub Desktop.
SVG Tools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
SVG parser for the Lasersaur. | |
Converts SVG DOM to a flat collection of paths. | |
Copyright (c) 2011 Nortd Labs | |
Open Source by the terms of the Gnu Public License (GPL3) or higher. | |
Code inspired by cake.js, canvg.js, svg2obj.py, and Squirtle. | |
Thank you for open sourcing your work! | |
Usage: | |
var boundarys = SVGReader.parse(svgstring, config) | |
Features: | |
* <svg> width and height, viewBox clipping. | |
* paths, rectangles, ellipses, circles, lines, polylines and polygons | |
* nested transforms | |
* transform lists (transform="rotate(30) translate(2,2) scale(4)") | |
* non-pixel units (cm, mm, in, pt, pc) | |
* 'style' attribute and presentation attributes | |
* curves, arcs, cirles, ellipses tesellated according to tolerance | |
Intentinally not Supported: | |
* markers | |
* masking | |
* em, ex, % units | |
* text (needs to be converted to paths) | |
* raster images | |
* style sheets | |
ToDo: | |
* check for out of bounds geometry | |
*/ | |
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | |
typeof define === 'function' && define.amd ? define(['exports'], factory) : | |
(factory((global.d3 = global.d3 || {}))); | |
}(this, (function (exports) { 'use strict'; | |
SVGReader = { | |
boundarys : {}, | |
// output path flattened (world coords) | |
// hash of path by color | |
// each path is a list of subpaths | |
// each subpath is a list of verteces | |
style : {}, | |
// style at current parsing position | |
tolerance : 0.1, | |
// max tollerance when tesselating curvy shapes | |
parse : function(svgstring, config) { | |
this.tolerance_squared = Math.pow(this.tolerance, 2); | |
// parse xml | |
var svgRootElement; | |
if (window.DOMParser) { | |
var parser = new DOMParser(); | |
svgRootElement = parser.parseFromString(svgstring, 'text/xml').documentElement; | |
} | |
else { | |
xml = xml.replace(/<!DOCTYPE svg[^>]*>/, ''); | |
var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); | |
xmlDoc.async = 'false'; | |
xmlDoc.loadXML(svgstring); | |
svgRootElement = xmlDoc.documentElement; | |
} | |
// let the fun begin | |
var node = {} | |
this.boundarys.allcolors = [] // TODO: sort by color | |
node.stroke = [255,0,0]; | |
node.xformToWorld = [1,0,0,1,0,0] | |
this.parseChildren(svgRootElement, node) | |
return this.boundarys | |
}, | |
parseChildren : function(domNode, parentNode) { | |
var childNodes = [] | |
for (var i=0; i<domNode.childNodes.length; i++) { | |
var tag = domNode.childNodes[i] | |
if (tag.childNodes) { | |
if (tag.tagName) { | |
// we are looping here through | |
// all nodes with child nodes | |
// others are irrelevant | |
// 1.) setup a new node | |
// and inherit from parent | |
var node = {} | |
node.path = []; | |
node.xform = [1,0,0,1,0,0]; | |
node.opacity = parentNode.opacity; | |
node.display = parentNode.display; | |
node.visibility = parentNode.visibility; | |
node.fill = parentNode.fill; | |
node.stroke = parentNode.stroke; | |
node.color = parentNode.color; | |
node.fillOpacity = parentNode.fillOpacity; | |
node.strokeOpacity = parentNode.strokeOpacity; | |
// 2.) parse own attributes and overwrite | |
if (tag.attributes) { | |
for (var j=0; j<tag.attributes.length; j++) { | |
var attr = tag.attributes[j] | |
if (attr.nodeName && attr.nodeValue && this.SVGAttributeMapping[attr.nodeName]) { | |
this.SVGAttributeMapping[attr.nodeName](this, node, attr.nodeValue) | |
} | |
} | |
} | |
// 3.) accumulate transformations | |
node.xformToWorld = this.matrixMult(parentNode.xformToWorld, node.xform) | |
// 4.) parse tag | |
// with current attributes and transformation | |
if (this.SVGTagMapping[tag.tagName]) { | |
//if (node.stroke[0] == 255 && node.stroke[1] == 0 && node.stroke[2] == 0) { | |
this.SVGTagMapping[tag.tagName](this, tag, node) | |
//} | |
} | |
// 5.) compile boundarys | |
// before adding all path data convert to world coordinates | |
for (var k=0; k<node.path.length; k++) { | |
var subpath = node.path[k]; | |
for (var l=0; l<node.path[k].length; l++) { | |
var tmp = this.matrixApply(node.xformToWorld, subpath[l]); | |
subpath[l] = new Vec2(tmp[0], tmp[1]); | |
} | |
subpath.node = node; | |
this.boundarys.allcolors.push(subpath); | |
} | |
} | |
// recursive call | |
this.parseChildren(tag, node) | |
} | |
} | |
}, | |
///////////////////////////// | |
// recognized svg attributes | |
SVGAttributeMapping : { | |
DEG_TO_RAD : Math.PI / 180, | |
RAD_TO_DEG : 180 / Math.PI, | |
id : function(parser, node, val) { | |
node.id = val | |
}, | |
transform : function(parser, node, val) { | |
// http://www.w3.org/TR/SVG11/coords.html#EstablishingANewUserSpace | |
var xforms = [] | |
var segs = val.match(/[a-z]+\s*\([^)]*\)/ig) | |
for (var i=0; i<segs.length; i++) { | |
var kv = segs[i].split("("); | |
var xformKind = kv[0].strip(); | |
var paramsTemp = kv[1].strip().slice(0,-1); | |
var params = paramsTemp.split(/[\s,]+/).map(parseFloat) | |
// double check params | |
for (var j=0; j<params.length; j++) { | |
if ( isNaN(params[j]) ) { | |
$().uxmessage('warning', 'transform skipped; contains non-numbers'); | |
continue // skip this transform | |
} | |
} | |
// translate | |
if (xformKind == 'translate') { | |
if (params.length == 1) { | |
xforms.push([1, 0, 0, 1, params[0], params[0]]) | |
} else if (params.length == 2) { | |
xforms.push([1, 0, 0, 1, params[0], params[1]]) | |
} else { | |
$().uxmessage('warning', 'translate skipped; invalid num of params'); | |
} | |
// rotate | |
} else if (xformKind == 'rotate') { | |
if (params.length == 3) { | |
var angle = params[0] * this.DEG_TO_RAD | |
xforms.push([1, 0, 0, 1, params[1], params[2]]) | |
xforms.push([Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0]) | |
xforms.push([1, 0, 0, 1, -params[1], -params[2]]) | |
} else if (params.length == 1) { | |
var angle = params[0] * this.DEG_TO_RAD | |
xforms.push([Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0]) | |
} else { | |
$().uxmessage('warning', 'rotate skipped; invalid num of params'); | |
} | |
//scale | |
} else if (xformKind == 'scale') { | |
if (params.length == 1) { | |
xforms.push([params[0], 0, 0, params[0], 0, 0]) | |
} else if (params.length == 2) { | |
xforms.push([params[0], 0, 0, params[1], 0, 0]) | |
} else { | |
$().uxmessage('warning', 'scale skipped; invalid num of params'); | |
} | |
// matrix | |
} else if (xformKind == 'matrix') { | |
if (params.length == 6) { | |
xforms.push(params) | |
} | |
// skewX | |
} else if (xformKind == 'skewX') { | |
if (params.length == 1) { | |
var angle = params[0]*this.DEG_TO_RAD | |
xforms.push([1, 0, Math.tan(angle), 1, 0, 0]) | |
} else { | |
$().uxmessage('warning', 'skewX skipped; invalid num of params'); | |
} | |
// skewY | |
} else if (xformKind == 'skewY') { | |
if (params.length == 1) { | |
var angle = params[0]*this.DEG_TO_RAD | |
xforms.push([1, Math.tan(angle), 0, 1, 0, 0]) | |
} else { | |
$().uxmessage('warning', 'skewY skipped; invalid num of params'); | |
} | |
} | |
} | |
//calculate combined transformation matrix | |
xform_combined = [1,0,0,1,0,0] | |
for (var i=0; i<xforms.length; i++) { | |
xform_combined = parser.matrixMult(xform_combined, xforms[i]) | |
} | |
// assign | |
node.xform = xform_combined | |
}, | |
style : function(parser, node, val) { | |
// style attribute | |
// http://www.w3.org/TR/SVG11/styling.html#StyleAttribute | |
// example: <rect x="200" y="100" width="600" height="300" | |
// style="fill: red; stroke: blue; stroke-width: 3"/> | |
// relay to parse style attributes the same as Presentation Attributes | |
var segs = val.split(";") | |
for (var i=0; i<segs.length; i++) { | |
var kv = segs[i].split(":") | |
var k = kv[0].strip() | |
if (this[k]) { | |
var v = kv[1].strip() | |
this[k](parser, node, v) | |
} | |
} | |
}, | |
/////////////////////////// | |
// Presentations Attributes | |
// http://www.w3.org/TR/SVG11/styling.html#UsingPresentationAttributes | |
// example: <rect x="200" y="100" width="600" height="300" | |
// fill="red" stroke="blue" stroke-width="3"/> | |
opacity : function(parser, node, val) { | |
node.opacity = parseFloat(val) | |
}, | |
display : function (parser, node, val) { | |
node.display = val | |
}, | |
visibility : function (parser, node, val) { | |
node.visibility = val | |
}, | |
fill : function(parser, node, val) { | |
node.fill = this.__parseColor(val, node.color) | |
}, | |
stroke : function(parser, node, val) { | |
node.stroke = this.__parseColor(val, node.color) | |
}, | |
color : function(parser, node, val) { | |
if (val == 'inherit') return | |
node.color = this.__parseColor(val, node.color) | |
}, | |
'fill-opacity' : function(parser, node, val) { | |
node.fillOpacity = Math.min(1,Math.max(0,parseFloat(val))) | |
}, | |
'stroke-opacity' : function(parser, node, val) { | |
node.strokeOpacity = Math.min(1,Math.max(0,parseFloat(val))) | |
}, | |
// Presentations Attributes | |
/////////////////////////// | |
__parseColor : function(val, currentColor) { | |
if (val.charAt(0) == '#') { | |
if (val.length == 4) | |
val = val.replace(/([^#])/g, '$1$1') | |
var a = val.slice(1).match(/../g).map( | |
function(i) { return parseInt(i, 16) }) | |
return a | |
} else if (val.search(/^rgb\(/) != -1) { | |
var a = val.slice(4,-1).split(",") | |
for (var i=0; i<a.length; i++) { | |
var c = a[i].strip() | |
if (c.charAt(c.length-1) == '%') | |
a[i] = Math.round(parseFloat(c.slice(0,-1)) * 2.55) | |
else | |
a[i] = parseInt(c) | |
} | |
return a | |
} else if (val.search(/^rgba\(/) != -1) { | |
var a = val.slice(5,-1).split(",") | |
for (var i=0; i<3; i++) { | |
var c = a[i].strip() | |
if (c.charAt(c.length-1) == '%') | |
a[i] = Math.round(parseFloat(c.slice(0,-1)) * 2.55) | |
else | |
a[i] = parseInt(c) | |
} | |
var c = a[3].strip() | |
if (c.charAt(c.length-1) == '%') | |
a[3] = Math.round(parseFloat(c.slice(0,-1)) * 0.01) | |
else | |
a[3] = Math.max(0, Math.min(1, parseFloat(c))) | |
return a | |
} else if (val.search(/^url\(/) != -1) { | |
$().uxmessage('error', "defs are not supported at the moment"); | |
} else if (val == 'currentColor') { | |
return currentColor | |
} else if (val == 'none') { | |
return 'none' | |
} else if (val == 'freeze') { // SMIL is evil, but so are we | |
return null | |
} else if (val == 'remove') { | |
return null | |
} else { // unknown value, maybe it's an ICC color | |
return val | |
} | |
} | |
}, | |
// recognized svg attributes | |
///////////////////////////// | |
/////////////////////////// | |
// recognized svg elements | |
SVGTagMapping : { | |
svg : function(parser, tag, node) { | |
// has style attributes | |
node.fill = 'black' | |
node.stroke = 'none' | |
// // parse document dimensions | |
// node.width = 0 | |
// node.height = 0 | |
// var w = tag.getAttribute('width') | |
// var h = tag.getAttribute('height') | |
// if (!w) w = h | |
// else if (!h) h = w | |
// if (w) { | |
// var wpx = parser.parseUnit(w, cn, 'x') | |
// var hpx = parser.parseUnit(h, cn, 'y') | |
// } | |
}, | |
g : function(parser, tag, node) { | |
// http://www.w3.org/TR/SVG11/struct.html#Groups | |
// has transform and style attributes | |
}, | |
polygon : function(parser, tag, node) { | |
// http://www.w3.org/TR/SVG11/shapes.html#PolygonElement | |
// has transform and style attributes | |
var d = this.__getPolyPath(tag) | |
d.push('z') | |
parser.addPath(d, node) | |
}, | |
polyline : function(parser, tag, node) { | |
// http://www.w3.org/TR/SVG11/shapes.html#PolylineElement | |
// has transform and style attributes | |
var d = this.__getPolyPath(tag) | |
parser.addPath(d, node) | |
}, | |
__getPolyPath : function(tag) { | |
// has transform and style attributes | |
var subpath = [] | |
var vertnums = tag.getAttribute("points").toString().strip().split(/[\s,]+/).map(parseFloat) | |
if (vertnums.length % 2 == 0) { | |
var d = ['M'] | |
d.push(vertnums[0]) | |
d.push(vertnums[1]) | |
for (var i=2; i<vertnums.length; i+=2) { | |
d.push(vertnums[i]) | |
d.push(vertnums[i+1]) | |
} | |
return d | |
} else { | |
$().uxmessage('error', "in __getPolyPath: odd number of verteces"); | |
} | |
}, | |
rect : function(parser, tag, node) { | |
// http://www.w3.org/TR/SVG11/shapes.html#RectElement | |
// has transform and style attributes | |
var w = parser.parseUnit(tag.getAttribute('width')) || 0 | |
var h = parser.parseUnit(tag.getAttribute('height')) || 0 | |
var x = parser.parseUnit(tag.getAttribute('x')) || 0 | |
var y = parser.parseUnit(tag.getAttribute('y')) || 0 | |
var rx = parser.parseUnit(tag.getAttribute('rx')) | |
var ry = parser.parseUnit(tag.getAttribute('ry')) | |
if(rx == null || ry == null) { // no rounded corners | |
var d = ['M', x, y, 'h', w, 'v', h, 'h', -w, 'z']; | |
parser.addPath(d, node) | |
} else { // rounded corners | |
if ('ry' == null) { ry = rx; } | |
if (rx < 0.0) { rx *=-1; } | |
if (ry < 0.0) { ry *=-1; } | |
d = ['M', x+rx , y , | |
'h', w-2*rx, | |
'c', rx, 0.0, rx, ry, rx, ry, | |
'v', h-ry, | |
'c', '0.0', ry, -rx, ry, -rx, ry, | |
'h', -w+2*rx, | |
'c', -rx, '0.0', -rx, -ry, -rx, -ry, | |
'v', -h+ry, | |
'c', '0.0','0.0','0.0', -ry, rx, -ry, | |
'z']; | |
parser.addPath(d, node) | |
} | |
}, | |
line : function(parser, tag, node) { | |
// http://www.w3.org/TR/SVG11/shapes.html#LineElement | |
// has transform and style attributes | |
var x1 = parser.parseUnit(tag.getAttribute('x1')) || 0 | |
var y1 = parser.parseUnit(tag.getAttribute('y1')) || 0 | |
var x2 = parser.parseUnit(tag.getAttribute('x2')) || 0 | |
var y2 = parser.parseUnit(tag.getAttribute('y2')) || 0 | |
var d = ['M', x1, y1, 'L', x2, y2] | |
parser.addPath(d, node) | |
}, | |
circle : function(parser, tag, node) { | |
// http://www.w3.org/TR/SVG11/shapes.html#CircleElement | |
// has transform and style attributes | |
var r = parser.parseUnit(tag.getAttribute('r')) | |
var cx = parser.parseUnit(tag.getAttribute('cx')) || 0 | |
var cy = parser.parseUnit(tag.getAttribute('cy')) || 0 | |
if (r > 0.0) { | |
var d = ['M', cx-r, cy, | |
'A', r, r, 0, 0, 0, cx, cy+r, | |
'A', r, r, 0, 0, 0, cx+r, cy, | |
'A', r, r, 0, 0, 0, cx, cy-r, | |
'A', r, r, 0, 0, 0, cx-r, cy, | |
'Z']; | |
parser.addPath(d, node); | |
} | |
}, | |
ellipse : function(parser, tag, node) { | |
// has transform and style attributes | |
var rx = parser.parseUnit(tag.getAttribute('rx')) | |
var ry = parser.parseUnit(tag.getAttribute('ry')) | |
var cx = parser.parseUnit(tag.getAttribute('cx')) || 0 | |
var cy = parser.parseUnit(tag.getAttribute('cy')) || 0 | |
if (rx > 0.0 && ry > 0.0) { | |
var d = ['M', cx-rx, cy, | |
'A', rx, ry, 0, 0, 0, cx, cy+ry, | |
'A', rx, ry, 0, 0, 0, cx+rx, cy, | |
'A', rx, ry, 0, 0, 0, cx, cy-ry, | |
'A', rx, ry, 0, 0, 0, cx-rx, cy, | |
'Z']; | |
parser.addPath(d, node); | |
} | |
}, | |
path : function(parser, tag, node) { | |
// http://www.w3.org/TR/SVG11/paths.html | |
// has transform and style attributes | |
var d = tag.getAttribute("d") | |
parser.addPath(d, node) | |
}, | |
image : function(parser, tag, node) { | |
// not supported | |
// has transform and style attributes | |
}, | |
defs : function(parser, tag, node) { | |
// not supported | |
// http://www.w3.org/TR/SVG11/struct.html#Head | |
// has transform and style attributes | |
}, | |
style : function(parser, tag, node) { | |
// not supported: embedded style sheets | |
// http://www.w3.org/TR/SVG11/styling.html#StyleElement | |
// instead presentation attributes and the 'style' attribute | |
// var style = tag.getAttribute("style") | |
// if (style) { | |
// var segs = style.split(";") | |
// for (var i=0; i<segs.length; i++) { | |
// var kv = segs[i].split(":") | |
// var k = kv[0].strip() | |
// if (this.SVGAttributeMapping[k]) { | |
// var v = kv[1].strip() | |
// this.SVGAttributeMapping[k].call(v, defs, st) | |
// } | |
// } | |
// } | |
} | |
}, | |
// recognized svg elements | |
/////////////////////////// | |
////////////////////////////////////////////////////////////////////////// | |
// handle path data | |
// this is where all the geometry gets converted for the boundarys output | |
addPath : function(d, node) { | |
// http://www.w3.org/TR/SVG11/paths.html#PathData | |
var tolerance2 = this.tolerance_squared | |
var totalMaxScale = this.matrixGetScale(node.xformToWorld); | |
if (totalMaxScale != 0) { | |
// adjust for possible transforms | |
tolerance2 /= Math.pow(totalMaxScale, 2); | |
// $().uxmessage('notice', "tolerance2: " + tolerance2.toString()); | |
} | |
if ( typeof d == 'string') { | |
// parse path string | |
d = d.match(/([A-Za-z]|-?[0-9]+\.?[0-9]*(?:e-?[0-9]*)?)/g); | |
for (var i=0; i<d.length; i++) { | |
var num = parseFloat(d[i]); | |
if (!isNaN(num)) { | |
d[i] = num; | |
} | |
} | |
} | |
//$().uxmessage('notice', "d: " + d.toString()); | |
function nextIsNum () { | |
return (d.length > 0) && (typeof(d[0]) === 'number'); | |
} | |
function getNext() { | |
if (d.length > 0) { | |
return d.shift(); // pop first item | |
} else { | |
$().uxmessage('error', "in addPath: not enough parameters"); | |
return null; | |
} | |
} | |
var x = 0; | |
var y = 0; | |
var cmdPrev = ''; | |
var xPrevCp; | |
var yPrevCp; | |
var subpath = []; | |
while (d.length > 0) { | |
var cmd = getNext(); | |
switch(cmd) { | |
case 'M': // moveto absolute | |
// start new subpath | |
if ( subpath.length > 0) { | |
node.path.push(subpath); | |
subpath = []; | |
} | |
var implicitVerts = 0 | |
while (nextIsNum()) { | |
x = getNext(); | |
y = getNext(); | |
subpath.push([x, y]); | |
implicitVerts += 1; | |
} | |
break | |
case 'm': //moveto relative | |
// start new subpath | |
if ( subpath.length > 0) { | |
node.path.push(subpath); | |
subpath = []; | |
} | |
if (cmdPrev == '') { | |
// first treated absolute | |
x = getNext(); | |
y = getNext(); | |
subpath.push([x, y]); | |
} | |
var implicitVerts = 0 | |
while (nextIsNum()) { | |
// subsequent treated realtive | |
x += getNext(); | |
y += getNext(); | |
subpath.push([x, y]); | |
implicitVerts += 1; | |
} | |
break; | |
case 'Z': // closepath | |
case 'z': // closepath | |
// loop and finalize subpath | |
if ( subpath.length > 0) { | |
subpath.push(subpath[0]); // close | |
node.path.push(subpath); | |
x = subpath[subpath.length-1][0]; | |
y = subpath[subpath.length-1][1]; | |
subpath = []; | |
} | |
break | |
case 'L': // lineto absolute | |
while (nextIsNum()) { | |
x = getNext(); | |
y = getNext(); | |
subpath.push([x, y]); | |
} | |
break | |
case 'l': // lineto relative | |
while (nextIsNum()) { | |
x += getNext(); | |
y += getNext(); | |
subpath.push([x, y]); | |
} | |
break | |
case 'H': // lineto horizontal absolute | |
while (nextIsNum()) { | |
x = getNext(); | |
subpath.push([x, y]); | |
} | |
break | |
case 'h': // lineto horizontal relative | |
while (nextIsNum()) { | |
x += getNext(); | |
subpath.push([x, y]); | |
} | |
break; | |
case 'V': // lineto vertical absolute | |
while (nextIsNum()) { | |
y = getNext() | |
subpath.push([x, y]) | |
} | |
break; | |
case 'v': // lineto vertical realtive | |
while (nextIsNum()) { | |
y += getNext(); | |
subpath.push([x, y]); | |
} | |
break; | |
case 'C': // curveto cubic absolute | |
while (nextIsNum()) { | |
var x2 = getNext(); | |
var y2 = getNext(); | |
var x3 = getNext(); | |
var y3 = getNext(); | |
var x4 = getNext(); | |
var y4 = getNext(); | |
subpath.push([x,y]); | |
this.addCubicBezier(subpath, x, y, x2, y2, x3, y3, x4, y4, 0, tolerance2); | |
subpath.push([x4,y4]); | |
x = x4; | |
y = y4; | |
xPrevCp = x3; | |
yPrevCp = y3; | |
} | |
break | |
case 'c': // curveto cubic relative | |
while (nextIsNum()) { | |
var x2 = x + getNext(); | |
var y2 = y + getNext(); | |
var x3 = x + getNext(); | |
var y3 = y + getNext(); | |
var x4 = x + getNext(); | |
var y4 = y + getNext(); | |
subpath.push([x,y]); | |
this.addCubicBezier(subpath, x, y, x2, y2, x3, y3, x4, y4, 0, tolerance2); | |
subpath.push([x4,y4]); | |
x = x4; | |
y = y4; | |
xPrevCp = x3; | |
yPrevCp = y3; | |
} | |
break | |
case 'S': // curveto cubic absolute shorthand | |
while (nextIsNum()) { | |
var x2; | |
var y2; | |
if (cmdPrev.match(/[CcSs]/)) { | |
x2 = x-(xPrevCp-x); | |
y2 = y-(yPrevCp-y); | |
} else { | |
x2 = x; | |
y2 = y; | |
} | |
var x3 = getNext(); | |
var y3 = getNext(); | |
var x4 = getNext(); | |
var y4 = getNext(); | |
subpath.push([x,y]); | |
this.addCubicBezier(subpath, x, y, x2, y2, x3, y3, x4, y4, 0, tolerance2); | |
subpath.push([x4,y4]); | |
x = x4; | |
y = y4; | |
xPrevCp = x3; | |
yPrevCp = y3; | |
} | |
break | |
case 's': // curveto cubic relative shorthand | |
while (nextIsNum()) { | |
var x2; | |
var y2; | |
if (cmdPrev.match(/[CcSs]/)) { | |
x2 = x-(xPrevCp-x); | |
y2 = y-(yPrevCp-y); | |
} else { | |
x2 = x; | |
y2 = y; | |
} | |
var x3 = x + getNext(); | |
var y3 = y + getNext(); | |
var x4 = x + getNext(); | |
var y4 = y + getNext(); | |
subpath.push([x,y]); | |
this.addCubicBezier(subpath, x, y, x2, y2, x3, y3, x4, y4, 0, tolerance2); | |
subpath.push([x4,y4]); | |
x = x4; | |
y = y4; | |
xPrevCp = x3; | |
yPrevCp = y3; | |
} | |
break | |
case 'Q': // curveto quadratic absolute | |
while (nextIsNum()) { | |
var x2 = getNext(); | |
var y2 = getNext(); | |
var x3 = getNext(); | |
var y3 = getNext(); | |
subpath.push([x,y]); | |
this.addQuadraticBezier(subpath, x, y, x2, y2, x3, y3, 0, tolerance2); | |
subpath.push([x3,y3]); | |
x = x3; | |
y = y3; | |
} | |
break | |
case 'q': // curveto quadratic relative | |
while (nextIsNum()) { | |
var x2 = x + getNext(); | |
var y2 = y + getNext(); | |
var x3 = x + getNext(); | |
var y3 = y + getNext(); | |
subpath.push([x,y]); | |
this.addQuadraticBezier(subpath, x, y, x2, y2, x3, y3, 0, tolerance2); | |
subpath.push([x3,y3]); | |
x = x3; | |
y = y3; | |
} | |
break | |
case 'T': // curveto quadratic absolute shorthand | |
while (nextIsNum()) { | |
var x2; | |
var y2; | |
if (cmdPrev.match(/[QqTt]/)) { | |
x2 = x-(xPrevCp-x); | |
y2 = y-(yPrevCp-y); | |
} else { | |
x2 = x; | |
y2 = y; | |
} | |
var x3 = getNext(); | |
var y3 = getNext(); | |
subpath.push([x,y]); | |
this.addQuadraticBezier(subpath, x, y, x2, y2, x3, y3, 0, tolerance2); | |
subpath.push([x3,y3]); | |
x = x3; | |
y = y3; | |
xPrevCp = x2; | |
yPrevCp = y2; | |
} | |
break | |
case 't': // curveto quadratic relative shorthand | |
while (nextIsNum()) { | |
var x2; | |
var y2; | |
if (cmdPrev.match(/[QqTt]/)) { | |
x2 = x-(xPrevCp-x); | |
y2 = y-(yPrevCp-y); | |
} else { | |
x2 = x; | |
y2 = y; | |
} | |
var x3 = x + getNext(); | |
var y3 = y + getNext(); | |
subpath.push([x,y]); | |
this.addQuadraticBezier(subpath, x, y, x2, y2, x3, y3, 0, tolerance2); | |
subpath.push([x3,y3]); | |
x = x3; | |
y = y3; | |
xPrevCp = x2; | |
yPrevCp = y2; | |
} | |
break | |
case 'A': // eliptical arc absolute | |
while (nextIsNum()) { | |
var rx = getNext(); | |
var ry = getNext(); | |
var xrot = getNext(); | |
var large = getNext(); | |
var sweep = getNext(); | |
var x2 = getNext(); | |
var y2 = getNext(); | |
this.addArc(subpath, x, y, rx, ry, xrot, large, sweep, x2, y2, tolerance2); | |
x = x2 | |
y = y2 | |
} | |
break | |
case 'a': // elliptical arc relative | |
while (nextIsNum()) { | |
var rx = getNext(); | |
var ry = getNext(); | |
var xrot = getNext(); | |
var large = getNext(); | |
var sweep = getNext(); | |
var x2 = x + getNext(); | |
var y2 = y + getNext(); | |
this.addArc(subpath, x, y, rx, ry, xrot, large, sweep, x2, y2, tolerance2); | |
x = x2 | |
y = y2 | |
} | |
break | |
} | |
cmdPrev = cmd; | |
} | |
// finalize subpath | |
if ( subpath.length > 0) { | |
node.path.push(subpath); | |
subpath = []; | |
} | |
}, | |
addCubicBezier : function(subpath, x1, y1, x2, y2, x3, y3, x4, y4, level, tolerance2) { | |
// for details see: | |
// http://www.antigrain.com/research/adaptive_bezier/index.html | |
// based on DeCasteljau Algorithm | |
// The reason we use a subdivision algo over an incremental one | |
// is we want to have control over the deviation to the curve. | |
// This mean we subdivide more and have more curve points in | |
// curvy areas and less in flatter areas of the curve. | |
if (level > 18) { | |
// protect from deep recursion cases | |
// max 2**18 = 262144 segments | |
return | |
} | |
// Calculate all the mid-points of the line segments | |
var x12 = (x1 + x2) / 2.0 | |
var y12 = (y1 + y2) / 2.0 | |
var x23 = (x2 + x3) / 2.0 | |
var y23 = (y2 + y3) / 2.0 | |
var x34 = (x3 + x4) / 2.0 | |
var y34 = (y3 + y4) / 2.0 | |
var x123 = (x12 + x23) / 2.0 | |
var y123 = (y12 + y23) / 2.0 | |
var x234 = (x23 + x34) / 2.0 | |
var y234 = (y23 + y34) / 2.0 | |
var x1234 = (x123 + x234) / 2.0 | |
var y1234 = (y123 + y234) / 2.0 | |
// Try to approximate the full cubic curve by a single straight line | |
var dx = x4-x1 | |
var dy = y4-y1 | |
var d2 = Math.abs(((x2 - x4) * dy - (y2 - y4) * dx)) | |
var d3 = Math.abs(((x3 - x4) * dy - (y3 - y4) * dx)) | |
if ( Math.pow(d2+d3, 2) < 5.0 * tolerance2 * (dx*dx + dy*dy) ) { | |
// added factor of 5.0 to match circle resolution | |
subpath.push([x1234, y1234]) | |
return | |
} | |
// Continue subdivision | |
this.addCubicBezier(subpath, x1, y1, x12, y12, x123, y123, x1234, y1234, level+1, tolerance2); | |
this.addCubicBezier(subpath, x1234, y1234, x234, y234, x34, y34, x4, y4, level+1, tolerance2); | |
}, | |
addQuadraticBezier : function(subpath, x1, y1, x2, y2, x3, y3, level, tolerance2) { | |
if (level > 18) { | |
// protect from deep recursion cases | |
// max 2**18 = 262144 segments | |
return | |
} | |
// Calculate all the mid-points of the line segments | |
var x12 = (x1 + x2) / 2.0 | |
var y12 = (y1 + y2) / 2.0 | |
var x23 = (x2 + x3) / 2.0 | |
var y23 = (y2 + y3) / 2.0 | |
var x123 = (x12 + x23) / 2.0 | |
var y123 = (y12 + y23) / 2.0 | |
var dx = x3-x1 | |
var dy = y3-y1 | |
var d = Math.abs(((x2 - x3) * dy - (y2 - y3) * dx)) | |
if ( d*d <= 5.0 * tolerance2 * (dx*dx + dy*dy) ) { | |
// added factor of 5.0 to match circle resolution | |
subpath.push([x123, y123]) | |
return | |
} | |
// Continue subdivision | |
this.addQuadraticBezier(subpath, x1, y1, x12, y12, x123, y123, level + 1, tolerance2) | |
this.addQuadraticBezier(subpath, x123, y123, x23, y23, x3, y3, level + 1, tolerance2) | |
}, | |
addArc : function(subpath, x1, y1, rx, ry, phi, large_arc, sweep, x2, y2, tolerance2) { | |
// Implemented based on the SVG implementation notes | |
// plus some recursive sugar for incrementally refining the | |
// arc resolution until the requested tolerance is met. | |
// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes | |
var cp = Math.cos(phi); | |
var sp = Math.sin(phi); | |
var dx = 0.5 * (x1 - x2); | |
var dy = 0.5 * (y1 - y2); | |
var x_ = cp * dx + sp * dy; | |
var y_ = -sp * dx + cp * dy; | |
var r2 = (Math.pow(rx*ry,2)-Math.pow(rx*y_,2)-Math.pow(ry*x_,2)) / | |
(Math.pow(rx*y_,2)+Math.pow(ry*x_,2)); | |
if (r2 < 0) { r2 = 0; } | |
var r = Math.sqrt(r2); | |
if (large_arc == sweep) { r = -r; } | |
var cx_ = r*rx*y_ / ry; | |
var cy_ = -r*ry*x_ / rx; | |
var cx = cp*cx_ - sp*cy_ + 0.5*(x1 + x2); | |
var cy = sp*cx_ + cp*cy_ + 0.5*(y1 + y2); | |
function angle(u, v) { | |
var a = Math.acos((u[0]*v[0] + u[1]*v[1]) / | |
Math.sqrt((Math.pow(u[0],2) + Math.pow(u[1],2)) * | |
(Math.pow(v[0],2) + Math.pow(v[1],2)))); | |
var sgn = -1; | |
if (u[0]*v[1] > u[1]*v[0]) { sgn = 1; } | |
return sgn * a; | |
} | |
var psi = angle([1,0], [(x_-cx_)/rx, (y_-cy_)/ry]); | |
var delta = angle([(x_-cx_)/rx, (y_-cy_)/ry], [(-x_-cx_)/rx, (-y_-cy_)/ry]); | |
if (sweep && delta < 0) { delta += Math.PI * 2; } | |
if (!sweep && delta > 0) { delta -= Math.PI * 2; } | |
function getVertex(pct) { | |
var theta = psi + delta * pct; | |
var ct = Math.cos(theta); | |
var st = Math.sin(theta); | |
return [cp*rx*ct-sp*ry*st+cx, sp*rx*ct+cp*ry*st+cy]; | |
} | |
// let the recursive fun begin | |
// | |
function recursiveArc(parser, t1, t2, c1, c5, level, tolerance2) { | |
if (level > 18) { | |
// protect from deep recursion cases | |
// max 2**18 = 262144 segments | |
return | |
} | |
var tRange = t2-t1 | |
var tHalf = t1 + 0.5*tRange; | |
var c2 = getVertex(t1 + 0.25*tRange); | |
var c3 = getVertex(tHalf); | |
var c4 = getVertex(t1 + 0.75*tRange); | |
if (parser.vertexDistanceSquared(c2, parser.vertexMiddle(c1,c3)) > tolerance2) { | |
recursiveArc(parser, t1, tHalf, c1, c3, level+1, tolerance2); | |
} | |
subpath.push(c3); | |
if (parser.vertexDistanceSquared(c4, parser.vertexMiddle(c3,c5)) > tolerance2) { | |
recursiveArc(parser, tHalf, t2, c3, c5, level+1, tolerance2); | |
} | |
} | |
var t1Init = 0.0; | |
var t2Init = 1.0; | |
var c1Init = getVertex(t1Init); | |
var c5Init = getVertex(t2Init); | |
subpath.push(c1Init); | |
recursiveArc(this, t1Init, t2Init, c1Init, c5Init, 0, tolerance2); | |
subpath.push(c5Init); | |
}, | |
// handle path data | |
////////////////////////////////////////////////////////////////////////// | |
parseUnit : function(val) { | |
if (val == null) { | |
return null | |
} else { | |
// assume 90dpi | |
var multiplier = 1.0 | |
if (val.search(/cm$/i) != -1) { | |
multiplier = 35.433070869 | |
} else if (val.search(/mm$/i) != -1) { | |
multiplier = 3.5433070869 | |
} else if (val.search(/pt$/i) != -1) { | |
multiplier = 1.25 | |
} else if (val.search(/pc$/i) != -1) { | |
multiplier = 15.0 | |
} else if (val.search(/in$/i) != -1) { | |
multiplier = 90.0 | |
} | |
return multiplier * parseFloat(val.strip()) | |
} | |
}, | |
matrixMult : function(mA, mB) { | |
return [ mA[0]*mB[0] + mA[2]*mB[1], | |
mA[1]*mB[0] + mA[3]*mB[1], | |
mA[0]*mB[2] + mA[2]*mB[3], | |
mA[1]*mB[2] + mA[3]*mB[3], | |
mA[0]*mB[4] + mA[2]*mB[5] + mA[4], | |
mA[1]*mB[4] + mA[3]*mB[5] + mA[5] ] | |
}, | |
matrixApply : function(mat, vec) { | |
return [ mat[0]*vec[0] + mat[2]*vec[1] + mat[4], | |
mat[1]*vec[0] + mat[3]*vec[1] + mat[5] ] ; | |
}, | |
matrixGetScale : function(mat) { | |
// extract absolute scale from matrix | |
var sx = Math.sqrt(mat[0]*mat[0] + mat[1]*mat[1]); | |
var sy = Math.sqrt(mat[2]*mat[2] + mat[3]*mat[3]); | |
// return dominant axis | |
if (sx > sy) { | |
return sx; | |
} else { | |
return sy; | |
} | |
}, | |
vertexDistanceSquared : function(v1, v2) { | |
return Math.pow(v2[0]-v1[0], 2) + Math.pow(v2[1]-v1[1], 2); | |
}, | |
vertexMiddle : function(v1, v2) { | |
return [ (v2[0]+v1[0])/2.0, (v2[1]+v1[1])/2.0 ]; | |
} | |
} | |
Object.defineProperty(exports, '__esModule', { value: true }); | |
}))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment