Skip to content

Instantly share code, notes, and snippets.

@dustMason
Last active October 3, 2018 05:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dustMason/03c8f136eb19eacda751ead8c6ab1fea to your computer and use it in GitHub Desktop.
Save dustMason/03c8f136eb19eacda751ead8c6ab1fea to your computer and use it in GitHub Desktop.
SVG Tools
/**
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