processing-js
Version:
For the project website, visit http://processingjs.org
1,091 lines (1,062 loc) • 40.5 kB
JavaScript
/**
* SVG stands for Scalable Vector Graphics, a portable graphics format. It is
* a vector format so it allows for infinite resolution and relatively small
* file sizes. Most modern media software can view SVG files, including Adobe
* products, Firefox, etc. Illustrator and Inkscape can edit SVG files.
*
* @param {PApplet} parent typically use "this"
* @param {String} filename name of the SVG file to load
* @param {XMLElement} xml an XMLElement element
* @param {PShapeSVG} parent the parent PShapeSVG
*
* @see PShape
*/
module.exports = function(options) {
var CommonFunctions = options.CommonFunctions,
PConstants = options.PConstants,
PShape = options.PShape,
XMLElement = options.XMLElement,
colors = options.colors;
var PShapeSVG = function() {
PShape.call(this); // PShape is the base class.
if (arguments.length === 1) { // xml element coming in
this.element = arguments[0];
// set values to their defaults according to the SVG spec
this.vertexCodes = [];
this.vertices = [];
this.opacity = 1;
this.stroke = false;
this.strokeColor = PConstants.ALPHA_MASK;
this.strokeWeight = 1;
this.strokeCap = PConstants.SQUARE; // BUTT in svg spec
this.strokeJoin = PConstants.MITER;
this.strokeGradient = null;
this.strokeGradientPaint = null;
this.strokeName = null;
this.strokeOpacity = 1;
this.fill = true;
this.fillColor = PConstants.ALPHA_MASK;
this.fillGradient = null;
this.fillGradientPaint = null;
this.fillName = null;
this.fillOpacity = 1;
if (this.element.getName() !== "svg") {
throw("root is not <svg>, it's <" + this.element.getName() + ">");
}
}
else if (arguments.length === 2) {
if (typeof arguments[1] === 'string') {
if (arguments[1].indexOf(".svg") > -1) { //its a filename
this.element = new XMLElement(true, arguments[1]);
// set values to their defaults according to the SVG spec
this.vertexCodes = [];
this.vertices = [];
this.opacity = 1;
this.stroke = false;
this.strokeColor = PConstants.ALPHA_MASK;
this.strokeWeight = 1;
this.strokeCap = PConstants.SQUARE; // BUTT in svg spec
this.strokeJoin = PConstants.MITER;
this.strokeGradient = "";
this.strokeGradientPaint = "";
this.strokeName = "";
this.strokeOpacity = 1;
this.fill = true;
this.fillColor = PConstants.ALPHA_MASK;
this.fillGradient = null;
this.fillGradientPaint = null;
this.fillOpacity = 1;
}
} else { // XMLElement
if (arguments[0]) { // PShapeSVG
this.element = arguments[1];
this.vertexCodes = arguments[0].vertexCodes.slice();
this.vertices = arguments[0].vertices.slice();
this.stroke = arguments[0].stroke;
this.strokeColor = arguments[0].strokeColor;
this.strokeWeight = arguments[0].strokeWeight;
this.strokeCap = arguments[0].strokeCap;
this.strokeJoin = arguments[0].strokeJoin;
this.strokeGradient = arguments[0].strokeGradient;
this.strokeGradientPaint = arguments[0].strokeGradientPaint;
this.strokeName = arguments[0].strokeName;
this.fill = arguments[0].fill;
this.fillColor = arguments[0].fillColor;
this.fillGradient = arguments[0].fillGradient;
this.fillGradientPaint = arguments[0].fillGradientPaint;
this.fillName = arguments[0].fillName;
this.strokeOpacity = arguments[0].strokeOpacity;
this.fillOpacity = arguments[0].fillOpacity;
this.opacity = arguments[0].opacity;
}
}
}
this.name = this.element.getStringAttribute("id");
var displayStr = this.element.getStringAttribute("display", "inline");
this.visible = displayStr !== "none";
var str = this.element.getAttribute("transform");
if (str) {
this.matrix = this.parseMatrix(str);
}
// not proper parsing of the viewBox, but will cover us for cases where
// the width and height of the object is not specified
var viewBoxStr = this.element.getStringAttribute("viewBox");
if ( viewBoxStr !== null ) {
var viewBox = viewBoxStr.split(" ");
this.width = viewBox[2];
this.height = viewBox[3];
}
// TODO if viewbox is not same as width/height, then use it to scale
// the original objects. for now, viewbox only used when width/height
// are empty values (which by the spec means w/h of "100%"
var unitWidth = this.element.getStringAttribute("width");
var unitHeight = this.element.getStringAttribute("height");
if (unitWidth !== null) {
this.width = this.parseUnitSize(unitWidth);
this.height = this.parseUnitSize(unitHeight);
} else {
if ((this.width === 0) || (this.height === 0)) {
// For the spec, the default is 100% and 100%. For purposes
// here, insert a dummy value because this is prolly just a
// font or something for which the w/h doesn't matter.
this.width = 1;
this.height = 1;
//show warning
throw("The width and/or height is not " +
"readable in the <svg> tag of this file.");
}
}
this.parseColors(this.element);
this.parseChildren(this.element);
};
/**
* PShapeSVG methods are inherited from the PShape prototype
*/
PShapeSVG.prototype = new PShape();
/**
* @member PShapeSVG
* The parseMatrix() function parses the specified SVG matrix into a PMatrix2D. Note that PMatrix2D
* is rotated relative to the SVG definition, so parameters are rearranged
* here. More about the transformation matrices in
* <a href="http://www.w3.org/TR/SVG/coords.html#TransformAttribute">this section</a>
* of the SVG documentation.
*
* @param {String} str text of the matrix param.
*
* @return {PMatrix2D} a PMatrix2D
*/
PShapeSVG.prototype.parseMatrix = (function() {
function getCoords(s) {
var m = [];
s.replace(/\((.*?)\)/, (function() {
return function(all, params) {
// get the coordinates that can be separated by spaces or a comma
m = params.replace(/,+/g, " ").split(/\s+/);
};
}()));
return m;
}
return function(str) {
this.checkMatrix(2);
var pieces = [];
str.replace(/\s*(\w+)\((.*?)\)/g, function(all) {
// get a list of transform definitions
pieces.push(CommonFunctions.trim(all));
});
if (pieces.length === 0) {
return null;
}
for (var i = 0, j = pieces.length; i < j; i++) {
var m = getCoords(pieces[i]);
if (pieces[i].indexOf("matrix") !== -1) {
this.matrix.set(m[0], m[2], m[4], m[1], m[3], m[5]);
} else if (pieces[i].indexOf("translate") !== -1) {
var tx = m[0];
var ty = (m.length === 2) ? m[1] : 0;
this.matrix.translate(tx,ty);
} else if (pieces[i].indexOf("scale") !== -1) {
var sx = m[0];
var sy = (m.length === 2) ? m[1] : m[0];
this.matrix.scale(sx,sy);
} else if (pieces[i].indexOf("rotate") !== -1) {
var angle = m[0];
if (m.length === 1) {
this.matrix.rotate(CommonFunctions.radians(angle));
} else if (m.length === 3) {
this.matrix.translate(m[1], m[2]);
this.matrix.rotate(CommonFunctions.radians(m[0]));
this.matrix.translate(-m[1], -m[2]);
}
} else if (pieces[i].indexOf("skewX") !== -1) {
this.matrix.skewX(parseFloat(m[0]));
} else if (pieces[i].indexOf("skewY") !== -1) {
this.matrix.skewY(m[0]);
} else if (pieces[i].indexOf("shearX") !== -1) {
this.matrix.shearX(m[0]);
} else if (pieces[i].indexOf("shearY") !== -1) {
this.matrix.shearY(m[0]);
}
}
return this.matrix;
};
}());
/**
* @member PShapeSVG
* The parseChildren() function parses the specified XMLElement
*
* @param {XMLElement}element the XMLElement to parse
*/
PShapeSVG.prototype.parseChildren = function(element) {
var newelement = element.getChildren();
var base = new PShape();
var i, j;
for (i = 0, j = newelement.length; i < j; i++) {
var kid = this.parseChild(newelement[i]);
if (kid) {
base.addChild(kid);
}
}
for (i = 0, j = base.children.length; i < j; i++) {
this.children.push(base.children[i]);
}
};
/**
* @member PShapeSVG
* The getName() function returns the name
*
* @return {String} the name
*/
PShapeSVG.prototype.getName = function() {
return this.name;
};
/**
* @member PShapeSVG
* The parseChild() function parses a child XML element.
*
* @param {XMLElement} elem the element to parse
*
* @return {PShape} the newly created PShape
*/
PShapeSVG.prototype.parseChild = function( elem ) {
var name = elem.getName();
var shape;
if (name === "g") {
shape = new PShapeSVG(this, elem);
} else if (name === "defs") {
// generally this will contain gradient info, so may
// as well just throw it into a group element for parsing
shape = new PShapeSVG(this, elem);
} else if (name === "line") {
shape = new PShapeSVG(this, elem);
shape.parseLine();
} else if (name === "circle") {
shape = new PShapeSVG(this, elem);
shape.parseEllipse(true);
} else if (name === "ellipse") {
shape = new PShapeSVG(this, elem);
shape.parseEllipse(false);
} else if (name === "rect") {
shape = new PShapeSVG(this, elem);
shape.parseRect();
} else if (name === "polygon") {
shape = new PShapeSVG(this, elem);
shape.parsePoly(true);
} else if (name === "polyline") {
shape = new PShapeSVG(this, elem);
shape.parsePoly(false);
} else if (name === "path") {
shape = new PShapeSVG(this, elem);
shape.parsePath();
} else if (name === "radialGradient") {
//return new RadialGradient(this, elem);
unimplemented('PShapeSVG.prototype.parseChild, name = radialGradient');
} else if (name === "linearGradient") {
//return new LinearGradient(this, elem);
unimplemented('PShapeSVG.prototype.parseChild, name = linearGradient');
} else if (name === "text") {
unimplemented('PShapeSVG.prototype.parseChild, name = text');
} else if (name === "filter") {
unimplemented('PShapeSVG.prototype.parseChild, name = filter');
} else if (name === "mask") {
unimplemented('PShapeSVG.prototype.parseChild, name = mask');
} else {
// ignoring
}
return shape;
};
/**
* @member PShapeSVG
* The parsePath() function parses the <path> element of the svg file
* A path is defined by including a path element which contains a d="(path data)" attribute, where the d attribute contains
* the moveto, line, curve (both cubic and quadratic Beziers), arc and closepath instructions.
**/
PShapeSVG.prototype.parsePath = function() {
this.family = PConstants.PATH;
this.kind = 0;
var pathDataChars = [];
var c;
//change multiple spaces and commas to single space
var pathData = CommonFunctions.trim(this.element.getStringAttribute("d").replace(/[\s,]+/g,' '));
if (pathData === null) {
return;
}
pathData = pathData.split('');
var cx = 0,
cy = 0,
ctrlX = 0,
ctrlY = 0,
ctrlX1 = 0,
ctrlX2 = 0,
ctrlY1 = 0,
ctrlY2 = 0,
endX = 0,
endY = 0,
ppx = 0,
ppy = 0,
px = 0,
py = 0,
i = 0,
valOf = 0;
var str = "";
var tmpArray = [];
var flag = false;
var lastInstruction;
var command;
var j, k;
while (i< pathData.length) {
valOf = pathData[i].charCodeAt(0);
if ((valOf >= 65 && valOf <= 90) || (valOf >= 97 && valOf <= 122)) {
// if it's a letter
// populate the tmpArray with coordinates
j = i;
i++;
if (i < pathData.length) { // don't go over boundary of array
tmpArray = [];
valOf = pathData[i].charCodeAt(0);
while (!((valOf >= 65 && valOf <= 90) ||
(valOf >= 97 && valOf <= 100) ||
(valOf >= 102 && valOf <= 122)) && flag === false) { // if its NOT a letter
if (valOf === 32) { //if its a space and the str isn't empty
// sometimes you get a space after the letter
if (str !== "") {
tmpArray.push(parseFloat(str));
str = "";
}
i++;
} else if (valOf === 45) { //if it's a -
// allow for 'e' notation in numbers, e.g. 2.10e-9
if (pathData[i-1].charCodeAt(0) === 101) {
str += pathData[i].toString();
i++;
} else {
// sometimes no space separator after (ex: 104.535-16.322)
if (str !== "") {
tmpArray.push(parseFloat(str));
}
str = pathData[i].toString();
i++;
}
} else {
str += pathData[i].toString();
i++;
}
if (i === pathData.length) { // don't go over boundary of array
flag = true;
} else {
valOf = pathData[i].charCodeAt(0);
}
}
}
if (str !== "") {
tmpArray.push(parseFloat(str));
str = "";
}
command = pathData[j];
valOf = command.charCodeAt(0);
if (valOf === 77) { // M - move to (absolute)
if (tmpArray.length >= 2 && tmpArray.length % 2 === 0) {
// need one+ pairs of co-ordinates
cx = tmpArray[0];
cy = tmpArray[1];
this.parsePathMoveto(cx, cy);
if (tmpArray.length > 2) {
for (j = 2, k = tmpArray.length; j < k; j+=2) {
// absolute line to
cx = tmpArray[j];
cy = tmpArray[j+1];
this.parsePathLineto(cx,cy);
}
}
}
} else if (valOf === 109) { // m - move to (relative)
if (tmpArray.length >= 2 && tmpArray.length % 2 === 0) {
// need one+ pairs of co-ordinates
cx += tmpArray[0];
cy += tmpArray[1];
this.parsePathMoveto(cx,cy);
if (tmpArray.length > 2) {
for (j = 2, k = tmpArray.length; j < k; j+=2) {
// relative line to
cx += tmpArray[j];
cy += tmpArray[j + 1];
this.parsePathLineto(cx,cy);
}
}
}
} else if (valOf === 76) { // L - lineto (absolute)
if (tmpArray.length >= 2 && tmpArray.length % 2 === 0) {
// need one+ pairs of co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=2) {
cx = tmpArray[j];
cy = tmpArray[j + 1];
this.parsePathLineto(cx,cy);
}
}
} else if (valOf === 108) { // l - lineto (relative)
if (tmpArray.length >= 2 && tmpArray.length % 2 === 0) {
// need one+ pairs of co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=2) {
cx += tmpArray[j];
cy += tmpArray[j+1];
this.parsePathLineto(cx,cy);
}
}
} else if (valOf === 72) { // H - horizontal lineto (absolute)
for (j = 0, k = tmpArray.length; j < k; j++) {
// multiple x co-ordinates can be provided
cx = tmpArray[j];
this.parsePathLineto(cx, cy);
}
} else if (valOf === 104) { // h - horizontal lineto (relative)
for (j = 0, k = tmpArray.length; j < k; j++) {
// multiple x co-ordinates can be provided
cx += tmpArray[j];
this.parsePathLineto(cx, cy);
}
} else if (valOf === 86) { // V - vertical lineto (absolute)
for (j = 0, k = tmpArray.length; j < k; j++) {
// multiple y co-ordinates can be provided
cy = tmpArray[j];
this.parsePathLineto(cx, cy);
}
} else if (valOf === 118) { // v - vertical lineto (relative)
for (j = 0, k = tmpArray.length; j < k; j++) {
// multiple y co-ordinates can be provided
cy += tmpArray[j];
this.parsePathLineto(cx, cy);
}
} else if (valOf === 67) { // C - curve to (absolute)
if (tmpArray.length >= 6 && tmpArray.length % 6 === 0) {
// need one+ multiples of 6 co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=6) {
ctrlX1 = tmpArray[j];
ctrlY1 = tmpArray[j + 1];
ctrlX2 = tmpArray[j + 2];
ctrlY2 = tmpArray[j + 3];
endX = tmpArray[j + 4];
endY = tmpArray[j + 5];
this.parsePathCurveto(ctrlX1,
ctrlY1,
ctrlX2,
ctrlY2,
endX,
endY);
cx = endX;
cy = endY;
}
}
} else if (valOf === 99) { // c - curve to (relative)
if (tmpArray.length >= 6 && tmpArray.length % 6 === 0) {
// need one+ multiples of 6 co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=6) {
ctrlX1 = cx + tmpArray[j];
ctrlY1 = cy + tmpArray[j + 1];
ctrlX2 = cx + tmpArray[j + 2];
ctrlY2 = cy + tmpArray[j + 3];
endX = cx + tmpArray[j + 4];
endY = cy + tmpArray[j + 5];
this.parsePathCurveto(ctrlX1,
ctrlY1,
ctrlX2,
ctrlY2,
endX,
endY);
cx = endX;
cy = endY;
}
}
} else if (valOf === 83) { // S - curve to shorthand (absolute)
if (tmpArray.length >= 4 && tmpArray.length % 4 === 0) {
// need one+ multiples of 4 co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=4) {
if (lastInstruction.toLowerCase() === "c" ||
lastInstruction.toLowerCase() === "s") {
ppx = this.vertices[ this.vertices.length-2 ][0];
ppy = this.vertices[ this.vertices.length-2 ][1];
px = this.vertices[ this.vertices.length-1 ][0];
py = this.vertices[ this.vertices.length-1 ][1];
ctrlX1 = px + (px - ppx);
ctrlY1 = py + (py - ppy);
} else {
//If there is no previous curve,
//the current point will be used as the first control point.
ctrlX1 = this.vertices[this.vertices.length-1][0];
ctrlY1 = this.vertices[this.vertices.length-1][1];
}
ctrlX2 = tmpArray[j];
ctrlY2 = tmpArray[j + 1];
endX = tmpArray[j + 2];
endY = tmpArray[j + 3];
this.parsePathCurveto(ctrlX1,
ctrlY1,
ctrlX2,
ctrlY2,
endX,
endY);
cx = endX;
cy = endY;
}
}
} else if (valOf === 115) { // s - curve to shorthand (relative)
if (tmpArray.length >= 4 && tmpArray.length % 4 === 0) {
// need one+ multiples of 4 co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=4) {
if (lastInstruction.toLowerCase() === "c" ||
lastInstruction.toLowerCase() === "s") {
ppx = this.vertices[this.vertices.length-2][0];
ppy = this.vertices[this.vertices.length-2][1];
px = this.vertices[this.vertices.length-1][0];
py = this.vertices[this.vertices.length-1][1];
ctrlX1 = px + (px - ppx);
ctrlY1 = py + (py - ppy);
} else {
//If there is no previous curve,
//the current point will be used as the first control point.
ctrlX1 = this.vertices[this.vertices.length-1][0];
ctrlY1 = this.vertices[this.vertices.length-1][1];
}
ctrlX2 = cx + tmpArray[j];
ctrlY2 = cy + tmpArray[j + 1];
endX = cx + tmpArray[j + 2];
endY = cy + tmpArray[j + 3];
this.parsePathCurveto(ctrlX1,
ctrlY1,
ctrlX2,
ctrlY2,
endX,
endY);
cx = endX;
cy = endY;
}
}
} else if (valOf === 81) { // Q - quadratic curve to (absolute)
if (tmpArray.length >= 4 && tmpArray.length % 4 === 0) {
// need one+ multiples of 4 co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=4) {
ctrlX = tmpArray[j];
ctrlY = tmpArray[j + 1];
endX = tmpArray[j + 2];
endY = tmpArray[j + 3];
this.parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY);
cx = endX;
cy = endY;
}
}
} else if (valOf === 113) { // q - quadratic curve to (relative)
if (tmpArray.length >= 4 && tmpArray.length % 4 === 0) {
// need one+ multiples of 4 co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=4) {
ctrlX = cx + tmpArray[j];
ctrlY = cy + tmpArray[j + 1];
endX = cx + tmpArray[j + 2];
endY = cy + tmpArray[j + 3];
this.parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY);
cx = endX;
cy = endY;
}
}
} else if (valOf === 84) {
// T - quadratic curve to shorthand (absolute)
if (tmpArray.length >= 2 && tmpArray.length % 2 === 0) {
// need one+ pairs of co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=2) {
if (lastInstruction.toLowerCase() === "q" ||
lastInstruction.toLowerCase() === "t") {
ppx = this.vertices[this.vertices.length-2][0];
ppy = this.vertices[this.vertices.length-2][1];
px = this.vertices[this.vertices.length-1][0];
py = this.vertices[this.vertices.length-1][1];
ctrlX = px + (px - ppx);
ctrlY = py + (py - ppy);
} else {
// If there is no previous command or if the previous command
// was not a Q, q, T or t, assume the control point is
// coincident with the current point.
ctrlX = cx;
ctrlY = cy;
}
endX = tmpArray[j];
endY = tmpArray[j + 1];
this.parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY);
cx = endX;
cy = endY;
}
}
} else if (valOf === 116) {
// t - quadratic curve to shorthand (relative)
if (tmpArray.length >= 2 && tmpArray.length % 2 === 0) {
// need one+ pairs of co-ordinates
for (j = 0, k = tmpArray.length; j < k; j+=2) {
if (lastInstruction.toLowerCase() === "q" ||
lastInstruction.toLowerCase() === "t") {
ppx = this.vertices[this.vertices.length-2][0];
ppy = this.vertices[this.vertices.length-2][1];
px = this.vertices[this.vertices.length-1][0];
py = this.vertices[this.vertices.length-1][1];
ctrlX = px + (px - ppx);
ctrlY = py + (py - ppy);
} else {
// If there is no previous command or if the previous command
// was not a Q, q, T or t, assume the control point is
// coincident with the current point.
ctrlX = cx;
ctrlY = cy;
}
endX = cx + tmpArray[j];
endY = cy + tmpArray[j + 1];
this.parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY);
cx = endX;
cy = endY;
}
}
} else if (valOf === 90 || valOf === 122) { // Z or z (these do the same thing)
this.close = true;
}
lastInstruction = command.toString();
} else { i++;}
}
};
/**
* @member PShapeSVG
* PShapeSVG.parsePath() helper function
*
* @see PShapeSVG#parsePath
*/
PShapeSVG.prototype.parsePathQuadto = function(x1, y1, cx, cy, x2, y2) {
if (this.vertices.length > 0) {
this.parsePathCode(PConstants.BEZIER_VERTEX);
// x1/y1 already covered by last moveto, lineto, or curveto
this.parsePathVertex(x1 + ((cx-x1)*2/3), y1 + ((cy-y1)*2/3));
this.parsePathVertex(x2 + ((cx-x2)*2/3), y2 + ((cy-y2)*2/3));
this.parsePathVertex(x2, y2);
} else {
throw ("Path must start with M/m");
}
};
/**
* @member PShapeSVG
* PShapeSVG.parsePath() helper function
*
* @see PShapeSVG#parsePath
*/
PShapeSVG.prototype.parsePathCurveto = function(x1, y1, x2, y2, x3, y3) {
if (this.vertices.length > 0) {
this.parsePathCode(PConstants.BEZIER_VERTEX );
this.parsePathVertex(x1, y1);
this.parsePathVertex(x2, y2);
this.parsePathVertex(x3, y3);
} else {
throw ("Path must start with M/m");
}
};
/**
* @member PShapeSVG
* PShapeSVG.parsePath() helper function
*
* @see PShapeSVG#parsePath
*/
PShapeSVG.prototype.parsePathLineto = function(px, py) {
if (this.vertices.length > 0) {
this.parsePathCode(PConstants.VERTEX);
this.parsePathVertex(px, py);
// add property to distinguish between curContext.moveTo
// or curContext.lineTo
this.vertices[this.vertices.length-1].moveTo = false;
} else {
throw ("Path must start with M/m");
}
};
PShapeSVG.prototype.parsePathMoveto = function(px, py) {
if (this.vertices.length > 0) {
this.parsePathCode(PConstants.BREAK);
}
this.parsePathCode(PConstants.VERTEX);
this.parsePathVertex(px, py);
// add property to distinguish between curContext.moveTo
// or curContext.lineTo
this.vertices[this.vertices.length-1].moveTo = true;
};
/**
* @member PShapeSVG
* PShapeSVG.parsePath() helper function
*
* @see PShapeSVG#parsePath
*/
PShapeSVG.prototype.parsePathVertex = function(x, y) {
var verts = [];
verts[0] = x;
verts[1] = y;
this.vertices.push(verts);
};
/**
* @member PShapeSVG
* PShapeSVG.parsePath() helper function
*
* @see PShapeSVG#parsePath
*/
PShapeSVG.prototype.parsePathCode = function(what) {
this.vertexCodes.push(what);
};
/**
* @member PShapeSVG
* The parsePoly() function parses a polyline or polygon from an SVG file.
*
* @param {boolean}val true if shape is closed (polygon), false if not (polyline)
*/
PShapeSVG.prototype.parsePoly = function(val) {
this.family = PConstants.PATH;
this.close = val;
var pointsAttr = CommonFunctions.trim(this.element.getStringAttribute("points").replace(/[,\s]+/g,' '));
if (pointsAttr !== null) {
//split into array
var pointsBuffer = pointsAttr.split(" ");
if (pointsBuffer.length % 2 === 0) {
for (var i = 0, j = pointsBuffer.length; i < j; i++) {
var verts = [];
verts[0] = pointsBuffer[i];
verts[1] = pointsBuffer[++i];
this.vertices.push(verts);
}
} else {
throw("Error parsing polygon points: odd number of coordinates provided");
}
}
};
/**
* @member PShapeSVG
* The parseRect() function parses a rect from an SVG file.
*/
PShapeSVG.prototype.parseRect = function() {
this.kind = PConstants.RECT;
this.family = PConstants.PRIMITIVE;
this.params = [];
this.params[0] = this.element.getFloatAttribute("x");
this.params[1] = this.element.getFloatAttribute("y");
this.params[2] = this.element.getFloatAttribute("width");
this.params[3] = this.element.getFloatAttribute("height");
if (this.params[2] < 0 || this.params[3] < 0) {
throw("svg error: negative width or height found while parsing <rect>");
}
};
/**
* @member PShapeSVG
* The parseEllipse() function handles parsing ellipse and circle tags.
*
* @param {boolean}val true if this is a circle and not an ellipse
*/
PShapeSVG.prototype.parseEllipse = function(val) {
this.kind = PConstants.ELLIPSE;
this.family = PConstants.PRIMITIVE;
this.params = [];
this.params[0] = this.element.getFloatAttribute("cx") | 0 ;
this.params[1] = this.element.getFloatAttribute("cy") | 0;
var rx, ry;
if (val) {
rx = ry = this.element.getFloatAttribute("r");
if (rx < 0) {
throw("svg error: negative radius found while parsing <circle>");
}
} else {
rx = this.element.getFloatAttribute("rx");
ry = this.element.getFloatAttribute("ry");
if (rx < 0 || ry < 0) {
throw("svg error: negative x-axis radius or y-axis radius found while parsing <ellipse>");
}
}
this.params[0] -= rx;
this.params[1] -= ry;
this.params[2] = rx*2;
this.params[3] = ry*2;
};
/**
* @member PShapeSVG
* The parseLine() function handles parsing line tags.
*
* @param {boolean}val true if this is a circle and not an ellipse
*/
PShapeSVG.prototype.parseLine = function() {
this.kind = PConstants.LINE;
this.family = PConstants.PRIMITIVE;
this.params = [];
this.params[0] = this.element.getFloatAttribute("x1");
this.params[1] = this.element.getFloatAttribute("y1");
this.params[2] = this.element.getFloatAttribute("x2");
this.params[3] = this.element.getFloatAttribute("y2");
};
/**
* @member PShapeSVG
* The parseColors() function handles parsing the opacity, strijem stroke-width, stroke-linejoin,stroke-linecap, fill, and style attributes
*
* @param {XMLElement}element the element of which attributes to parse
*/
PShapeSVG.prototype.parseColors = function(element) {
if (element.hasAttribute("opacity")) {
this.setOpacity(element.getAttribute("opacity"));
}
if (element.hasAttribute("stroke")) {
this.setStroke(element.getAttribute("stroke"));
}
if (element.hasAttribute("stroke-width")) {
// if NaN (i.e. if it's 'inherit') then default
// back to the inherit setting
this.setStrokeWeight(element.getAttribute("stroke-width"));
}
if (element.hasAttribute("stroke-linejoin") ) {
this.setStrokeJoin(element.getAttribute("stroke-linejoin"));
}
if (element.hasAttribute("stroke-linecap")) {
this.setStrokeCap(element.getStringAttribute("stroke-linecap"));
}
// fill defaults to black (though stroke defaults to "none")
// http://www.w3.org/TR/SVG/painting.html#FillProperties
if (element.hasAttribute("fill")) {
this.setFill(element.getStringAttribute("fill"));
}
if (element.hasAttribute("style")) {
var styleText = element.getStringAttribute("style");
var styleTokens = styleText.toString().split( ";" );
for (var i = 0, j = styleTokens.length; i < j; i++) {
var tokens = CommonFunctions.trim(styleTokens[i].split( ":" ));
if (tokens[0] === "fill") {
this.setFill(tokens[1]);
} else if (tokens[0] === "fill-opacity") {
this.setFillOpacity(tokens[1]);
} else if (tokens[0] === "stroke") {
this.setStroke(tokens[1]);
} else if (tokens[0] === "stroke-width") {
this.setStrokeWeight(tokens[1]);
} else if (tokens[0] === "stroke-linecap") {
this.setStrokeCap(tokens[1]);
} else if (tokens[0] === "stroke-linejoin") {
this.setStrokeJoin(tokens[1]);
} else if (tokens[0] === "stroke-opacity") {
this.setStrokeOpacity(tokens[1]);
} else if (tokens[0] === "opacity") {
this.setOpacity(tokens[1]);
} // Other attributes are not yet implemented
}
}
};
/**
* @member PShapeSVG
* PShapeSVG.parseColors() helper function
*
* @param {String} opacityText the value of fillOpacity
*
* @see PShapeSVG#parseColors
*/
PShapeSVG.prototype.setFillOpacity = function(opacityText) {
this.fillOpacity = parseFloat(opacityText);
this.fillColor = this.fillOpacity * 255 << 24 |
this.fillColor & 0xFFFFFF;
};
/**
* @member PShapeSVG
* PShapeSVG.parseColors() helper function
*
* @param {String} fillText the value of fill
*
* @see PShapeSVG#parseColors
*/
PShapeSVG.prototype.setFill = function (fillText) {
var opacityMask = this.fillColor & 0xFF000000;
if (fillText === "none") {
this.fill = false;
} else if (fillText.indexOf("#") === 0) {
this.fill = true;
if (fillText.length === 4) {
// convert #00F to #0000FF
fillText = fillText.replace(/#(.)(.)(.)/,"#$1$1$2$2$3$3");
}
this.fillColor = opacityMask |
(parseInt(fillText.substring(1), 16 )) &
0xFFFFFF;
} else if (fillText.indexOf("rgb") === 0) {
this.fill = true;
this.fillColor = opacityMask | this.parseRGB(fillText);
} else if (fillText.indexOf("url(#") === 0) {
this.fillName = fillText.substring(5, fillText.length - 1 );
} else if (colors[fillText]) {
this.fill = true;
this.fillColor = opacityMask |
(parseInt(colors[fillText].substring(1), 16)) &
0xFFFFFF;
}
};
/**
* @member PShapeSVG
* PShapeSVG.parseColors() helper function
*
* @param {String} opacity the value of opacity
*
* @see PShapeSVG#parseColors
*/
PShapeSVG.prototype.setOpacity = function(opacity) {
this.strokeColor = parseFloat(opacity) * 255 << 24 |
this.strokeColor & 0xFFFFFF;
this.fillColor = parseFloat(opacity) * 255 << 24 |
this.fillColor & 0xFFFFFF;
};
/**
* @member PShapeSVG
* PShapeSVG.parseColors() helper function
*
* @param {String} strokeText the value to set stroke to
*
* @see PShapeSVG#parseColors
*/
PShapeSVG.prototype.setStroke = function(strokeText) {
var opacityMask = this.strokeColor & 0xFF000000;
if (strokeText === "none") {
this.stroke = false;
} else if (strokeText.charAt( 0 ) === "#") {
this.stroke = true;
if (strokeText.length === 4) {
// convert #00F to #0000FF
strokeText = strokeText.replace(/#(.)(.)(.)/,"#$1$1$2$2$3$3");
}
this.strokeColor = opacityMask |
(parseInt( strokeText.substring( 1 ), 16 )) &
0xFFFFFF;
} else if (strokeText.indexOf( "rgb" ) === 0 ) {
this.stroke = true;
this.strokeColor = opacityMask | this.parseRGB(strokeText);
} else if (strokeText.indexOf( "url(#" ) === 0) {
this.strokeName = strokeText.substring(5, strokeText.length - 1);
} else if (colors[strokeText]) {
this.stroke = true;
this.strokeColor = opacityMask |
(parseInt(colors[strokeText].substring(1), 16)) &
0xFFFFFF;
}
};
/**
* @member PShapeSVG
* PShapeSVG.parseColors() helper function
*
* @param {String} weight the value to set strokeWeight to
*
* @see PShapeSVG#parseColors
*/
PShapeSVG.prototype.setStrokeWeight = function(weight) {
this.strokeWeight = this.parseUnitSize(weight);
};
/**
* @member PShapeSVG
* PShapeSVG.parseColors() helper function
*
* @param {String} linejoin the value to set strokeJoin to
*
* @see PShapeSVG#parseColors
*/
PShapeSVG.prototype.setStrokeJoin = function(linejoin) {
if (linejoin === "miter") {
this.strokeJoin = PConstants.MITER;
} else if (linejoin === "round") {
this.strokeJoin = PConstants.ROUND;
} else if (linejoin === "bevel") {
this.strokeJoin = PConstants.BEVEL;
}
};
/**
* @member PShapeSVG
* PShapeSVG.parseColors() helper function
*
* @param {String} linecap the value to set strokeCap to
*
* @see PShapeSVG#parseColors
*/
PShapeSVG.prototype.setStrokeCap = function (linecap) {
if (linecap === "butt") {
this.strokeCap = PConstants.SQUARE;
} else if (linecap === "round") {
this.strokeCap = PConstants.ROUND;
} else if (linecap === "square") {
this.strokeCap = PConstants.PROJECT;
}
};
/**
* @member PShapeSVG
* PShapeSVG.parseColors() helper function
*
* @param {String} opacityText the value to set stroke opacity to
*
* @see PShapeSVG#parseColors
*/
PShapeSVG.prototype.setStrokeOpacity = function (opacityText) {
this.strokeOpacity = parseFloat(opacityText);
this.strokeColor = this.strokeOpacity * 255 << 24 |
this.strokeColor &
0xFFFFFF;
};
/**
* @member PShapeSVG
* The parseRGB() function parses an rbg() color string and returns a color int
*
* @param {String} color the color to parse in rbg() format
*
* @return {int} the equivalent color int
*/
PShapeSVG.prototype.parseRGB = function(color) {
var sub = color.substring(color.indexOf('(') + 1, color.indexOf(')'));
var values = sub.split(", ");
return (values[0] << 16) | (values[1] << 8) | (values[2]);
};
/**
* @member PShapeSVG
* The parseUnitSize() function parse a size that may have a suffix for its units.
* Ignoring cases where this could also be a percentage.
* The <A HREF="http://www.w3.org/TR/SVG/coords.html#Units">units</A> spec:
* <UL>
* <LI>"1pt" equals "1.25px" (and therefore 1.25 user units)
* <LI>"1pc" equals "15px" (and therefore 15 user units)
* <LI>"1mm" would be "3.543307px" (3.543307 user units)
* <LI>"1cm" equals "35.43307px" (and therefore 35.43307 user units)
* <LI>"1in" equals "90px" (and therefore 90 user units)
* </UL>
*/
PShapeSVG.prototype.parseUnitSize = function (text) {
var len = text.length - 2;
if (len < 0) { return text; }
if (text.indexOf("pt") === len) {
return parseFloat(text.substring(0, len)) * 1.25;
}
if (text.indexOf("pc") === len) {
return parseFloat( text.substring( 0, len)) * 15;
}
if (text.indexOf("mm") === len) {
return parseFloat( text.substring(0, len)) * 3.543307;
}
if (text.indexOf("cm") === len) {
return parseFloat(text.substring(0, len)) * 35.43307;
}
if (text.indexOf("in") === len) {
return parseFloat(text.substring(0, len)) * 90;
}
if (text.indexOf("px") === len) {
return parseFloat(text.substring(0, len));
}
return parseFloat(text);
};
return PShapeSVG;
};