/*
Candy 2 - SVG Importer for Processing - http://processing.org
Copyright (c) 2006 Michael Chang (Flux)
http://www.ghost-hack.com/
Revised and expanded by Ben Fry for inclusion as a core library
Copyright (c) 2006-08 Ben Fry and Casey Reas
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 2.1 as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*/
package processing.candy;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.Hashtable;
import processing.core.*;
import processing.xml.*;
/**
* Candy is a minimal SVG import library for Processing.
* Candy was written by Michael Chang, and later revised and
* expanded for use as a Processing core library by Ben Fry.
*
* SVG stands for Scalable Vector Graphics, a portable graphics
* format. It is a vector format so it allows for infinite resolution
* and relatively minute file sizes. Most modern media software
* can view SVG files, including Firefox, Adobe products, etc.
* You can use something like Illustrator to edit SVG files.
*
* We have no intention of turning this into a full-featured SVG library.
* The goal of this project is a basic shape importer that is small enough
* to be included with applets, meaning that its download size should be
* in the neighborhood of 25-30k. Because of this size, it is not made part
* of processing.core, because it would increase the download size of any
* applet by 20%, and it's not a feature that will be used by the majority
* of our audience. For more sophisticated import/export, consider the
* Batik library
* from the Apache Software Foundation. Future improvements to this
* library may focus on this properly supporting a specific subset of
* SVG, for instance the simpler SVG profiles known as
* SVG Tiny or Basic ,
* although we still would not support the interactivity options.
*
* This library was specifically tested under SVG files created with Adobe
* Illustrator. We can't guarantee that it will work for any SVGs created with
* other software. In the future we would like to improve compatibility with
* Open Source software such as InkScape, however initial tests show its
* base implementation produces more complicated files, and this will require
* more time.
*
* An SVG created under Illustrator must be created in one of two ways:
*
* File → Save for Web (or control-alt-shift-s on a PC). Under
* settings, make sure the CSS properties is set to "Presentation Attributes".
* With Illustrator CS2, it is also possible to use "Save As" with "SVG"
* as the file setting, but the CSS properties should also be set similarly.
*
* Saving it any other way will most likely break Candy.
*
*
*
* A minimal example program using Candy:
* (assuming a working moo.svg is in your data folder)
*
*
* import processing.candy.*;
* import processing.xml.*;
*
* SVG moo;
* void setup() {
* size(400,400);
* moo = new SVG("moo.svg",this);
* }
* void draw() {
* moo.draw();
* }
*
*
* Note that processing.xml needs to be imported as well.
* This may not be required when running code within the Processing
* environment, but when exported it may cause a NoClassDefError.
* This will be fixed in later releases of Processing
* (Bug 518 ).
*
*
*
* February 2008 revisions by fry (Processing 0136)
*
* Added support for quadratic curves in paths (Q, q, T, and t operators)
* Support for reading SVG font data (though not rendering it yet)
*
*
* Revisions for "Candy 2" November 2006 by fry
*
* Switch to the new processing.xml library
* Several bug fixes for parsing of shape data
* Support for linear and radial gradients
* Support for additional types of shapes
* Added compound shapes (shapes with interior points)
* Added methods to get shapes from an internal table
*
*
* Revision 10/31/06 by flux
*
* Now properly supports Processing 0118
* Fixed a bunch of things for Casey's students and general buggity.
* Will now properly draw #FFFFFFFF colors (were being represented as -1)
* SVGs without tags are now properly caught and loaded
* Added a method customStyle() for overriding SVG colors/styles
* Added a method SVGStyle() to go back to using SVG colors/styles
*
*
* Some SVG objects and features may not yet be supported.
* Here is a partial list of non-included features
*
* Rounded rectangles
* Drop shadow objects
* Typography
* Layers added for Candy 2
* Patterns
* Embedded images
*
*
* For those interested, the SVG specification can be found
* here .
*/
public class SVG {
protected PApplet parent;
public float width;
public float height;
protected Hashtable table = new Hashtable();
protected XMLElement svg;
protected BaseObject root;
protected boolean ignoreStyles = false;
int drawMode = PConstants.CORNER;
/**
* Initializes a new SVG Object with the given filename.
*/
public SVG(PApplet parent, String filename) {
// this will grab the root document, starting
// the xml version and initial comments are ignored
this(parent, new XMLElement(parent, filename));
}
/**
* Initializes a new SVG Object with the given filename.
*/
public SVG(PApplet parent, XMLElement svg) {
this.parent = parent;
this.svg = svg;
if (!svg.getName().equals("svg")) {
throw new RuntimeException("root is not , it's <" + svg.getName() + ">");
}
// not proper parsing of the viewBox, but will cover us for cases where
// the width and height of the object is not specified
String viewBoxStr = svg.getStringAttribute("viewBox");
if (viewBoxStr != null) {
int[] viewBox = PApplet.parseInt(PApplet.splitTokens(viewBoxStr));
width = viewBox[2];
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%"
String unitWidth = svg.getStringAttribute("width");
String unitHeight = svg.getStringAttribute("height");
if (unitWidth != null) {
width = parseUnitSize(unitWidth);
height = parseUnitSize(unitHeight);
} else {
if ((width == 0) || (height == 0)) {
//throw new RuntimeException("width/height not specified");
System.err.println("The width and/or height is not " +
"readable in the tag of this file.");
// 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.
width = 1;
height = 1;
}
}
/*
PApplet.println("document has " + document.getChildCount() + " children");
//Get the xml child node we need
XMLElement doc = document.getChild(1);
PApplet.println(doc);
if (true) return;
*/
/*
//XMLElement entSVG = doc.getChild(0);
//XMLElement svg = entSVG.getChild(1);
//While we're doing that, save the width and height too
//svgWidth = svg.getIntAttribute("width");
//svgHeight = svg.getIntAttribute("height");
//Catch exception when SVG doesn't have a tag
XMLElement graphics;
String nameOfFirstChild = svg.getChild(1).toString();
if(nameOfFirstChild.equals(""))
graphics = svg.getChild(1);
else
graphics = svg;
this.svgData = svg;
*/
//parseChildren(document);
root = new Group(null, svg);
/*
XMLElement graphics = null;
//Print SVG on construction
//Use this for debugging
//svg.printElementTree(" .");
*/
}
/**
* Internal method used to clone an object and return the subtree.
*/
protected SVG(PApplet parent, float width, float height, Hashtable table,
BaseObject obj, boolean styleOverride) {
this.parent = parent;
this.width = width;
this.height = height;
this.table = table;
this.root = obj;
this.svg = obj.element;
this.ignoreStyles = styleOverride;
}
/**
* Parse a size that may have a suffix for its units.
* Ignoring cases where this could also be a percentage.
* The units spec:
*
* "1pt" equals "1.25px" (and therefore 1.25 user units)
* "1pc" equals "15px" (and therefore 15 user units)
* "1mm" would be "3.543307px" (3.543307 user units)
* "1cm" equals "35.43307px" (and therefore 35.43307 user units)
* "1in" equals "90px" (and therefore 90 user units)
*
*/
public float parseUnitSize(String text) {
int len = text.length() - 2;
if (text.endsWith("pt")) {
return PApplet.parseFloat(text.substring(0, len)) * 1.25f;
} else if (text.endsWith("pc")) {
return PApplet.parseFloat(text.substring(0, len)) * 15;
} else if (text.endsWith("mm")) {
return PApplet.parseFloat(text.substring(0, len)) * 3.543307f;
} else if (text.endsWith("cm")) {
return PApplet.parseFloat(text.substring(0, len)) * 35.43307f;
} else if (text.endsWith("in")) {
return PApplet.parseFloat(text.substring(0, len)) * 90;
} else if (text.endsWith("px")) {
return PApplet.parseFloat(text.substring(0, len));
} else {
return PApplet.parseFloat(text);
}
}
/**
* Get a particular element based on its SVG ID. When editing SVG by hand,
* this is the id="" tag on any SVG element. When editing from Illustrator,
* these IDs can be edited by expanding the layers palette. The names used
* in the layers palette, both for the layers or the shapes and groups
* beneath them can be used here.
*
* // This code grabs "Layer 3" and the shapes beneath it.
* SVG layer3 = svg.get("Layer 3");
*
*/
public SVG get(String name) {
BaseObject obj = (BaseObject) table.get(name);
if (obj == null) {
// try with underscores instead of spaces
obj = (BaseObject) table.get(name.replace(' ', '_'));
}
if (obj != null) {
return new SVG(parent, width, height, table, obj, ignoreStyles);
}
return null;
}
// grab the (fill) gradient from a particular object by name
// and apply it to either the stroke or fill
// based on
protected Paint getGradient(String name, float cx, float cy, float r) {
BaseObject obj = (BaseObject) table.get(name);
if (obj == null) {
// try with underscores instead of spaces
obj = (BaseObject) table.get(name.replace(' ', '_'));
}
if (obj != null) {
if (obj.fillGradient != null) {
return obj.calcGradientPaint(obj.fillGradient, cx, cy, r);
}
}
throw new RuntimeException("No gradient found for shape " + name);
}
protected Paint getGradient(String name, float x1, float y1, float x2, float y2) {
BaseObject obj = (BaseObject) table.get(name);
if (obj == null) {
// try with underscores instead of spaces
obj = (BaseObject) table.get(name.replace(' ', '_'));
}
if (obj != null) {
if (obj.fillGradient != null) {
return obj.calcGradientPaint(obj.fillGradient, x1, y1, x2, y2);
}
}
throw new RuntimeException("No gradient found for shape " + name);
}
public void strokeGradient(String name, float x, float y, float r) {
Paint paint = getGradient(name, x, y, r);
if (parent.g instanceof PGraphicsJava2D) {
PGraphicsJava2D p2d = ((PGraphicsJava2D) parent.g);
p2d.strokeGradient = true;
p2d.strokeGradientObject = paint;
}
}
public void strokeGradient(String name, float x1, float y1, float x2, float y2) {
Paint paint = getGradient(name, x1, y1, x2, y2);
if (parent.g instanceof PGraphicsJava2D) {
PGraphicsJava2D p2d = ((PGraphicsJava2D) parent.g);
p2d.strokeGradient = true;
p2d.strokeGradientObject = paint;
}
}
public void fillGradient(String name, float x, float y, float r) {
Paint paint = getGradient(name, x, y, r);
if (parent.g instanceof PGraphicsJava2D) {
PGraphicsJava2D p2d = ((PGraphicsJava2D) parent.g);
p2d.fillGradient = true;
p2d.fillGradientObject = paint;
}
}
public void fillGradient(String name, float x1, float y1, float x2, float y2) {
Paint paint = getGradient(name, x1, y1, x2, y2);
if (parent.g instanceof PGraphicsJava2D) {
PGraphicsJava2D p2d = ((PGraphicsJava2D) parent.g);
p2d.fillGradient = true;
p2d.fillGradientObject = paint;
}
}
/**
* Temporary hack for gradient handling. This is not supported
* and will be removed from future releases.
*/
/*
public void drawStyles() {
root.drawStyles();
//PApplet.println(root);
if (root instanceof VectorObject) {
((VectorObject)root).drawStyles();
} else {
PApplet.println("Only use drawStyles() on an object, not a group.");
}
}
*/
public void draw() {
if (drawMode == PConstants.CENTER) {
parent.pushMatrix();
parent.translate(-width/2, -height/2);
drawImpl();
parent.popMatrix();
} else if ((drawMode == PConstants.CORNER) ||
(drawMode == PConstants.CORNERS)) {
drawImpl();
}
}
/**
* Convenience method to draw at a particular location.
*/
public void draw(float x, float y) {
parent.pushMatrix();
if (drawMode == PConstants.CENTER) {
parent.translate(x - width/2, y - height/2);
} else if ((drawMode == PConstants.CORNER) ||
(drawMode == PConstants.CORNERS)) {
parent.translate(x, y);
}
drawImpl();
parent.popMatrix();
}
public void draw(float x, float y, float c, float d) {
parent.pushMatrix();
if (drawMode == PConstants.CENTER) {
// x and y are center, c and d refer to a diameter
parent.translate(x - c/2f, y - d/2f);
parent.scale(c / width, d / height);
} else if (drawMode == PConstants.CORNER) {
parent.translate(x, y);
parent.scale(c / width, d / height);
} else if (drawMode == PConstants.CORNERS) {
// c and d are x2/y2, make them into width/height
c -= x;
d -= y;
// then same as above
parent.translate(x, y);
parent.scale(c / width, d / height);
}
drawImpl();
parent.popMatrix();
}
/**
* Draws the SVG document.
*/
public void drawImpl() {
boolean stroke = parent.g.stroke;
int strokeColor = parent.g.strokeColor;
float strokeWeight = parent.g.strokeWeight;
int strokeCap = parent.g.strokeCap;
int strokeJoin= parent.g.strokeJoin;
boolean fill = parent.g.fill;
int fillColor = parent.g.fillColor;
int ellipseMode = parent.g.ellipseMode;
root.draw();
parent.g.stroke = stroke;
parent.g.strokeColor = strokeColor;
parent.g.strokeWeight = strokeWeight;
parent.g.strokeCap = strokeCap;
parent.g.strokeJoin = strokeJoin;
parent.g.fill = fill;
parent.g.fillColor = fillColor;
parent.g.ellipseMode = ellipseMode;
}
/**
* Set the orientation for drawn objects, similar to PImage.imageMode().
* @param which Either CORNER, CORNERS, or CENTER.
*/
public void drawMode(int which) {
drawMode = which;
}
/**
* Overrides SVG-set styles and uses PGraphics styles and colors.
* Identical to ignoreStyles(true).
*/
public void ignoreStyles() {
ignoreStyles(true);
}
/**
* Enables or disables style information (fill and stroke) set in the file.
* @param state true to use user-specified stroke/fill, false for svg version
*/
public void ignoreStyles(boolean state) {
ignoreStyles = state;
}
/**
* Prints out the SVG document useful for parsing
*/
public void print() {
PApplet.println(svg.toString());
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
protected abstract class BaseObject {
String id;
XMLElement element;
// set to false if the object is hidden in the layers palette
boolean display;
boolean stroke;
int strokeColor;
float strokeWeight; // default is 1
int strokeCap;
int strokeJoin;
Gradient strokeGradient;
Paint strokeGradientPaint;
String strokeName; // id of another object, gradients only?
boolean fill;
int fillColor;
Gradient fillGradient;
Paint fillGradientPaint;
String fillName; // id of another object
boolean hasTransform;
PMatrix transformation;
float strokeOpacity;
float fillOpacity;
float opacity;
public BaseObject(BaseObject parent, XMLElement properties) {
if (parent == null) {
// set values to their defaults according to the SVG spec
stroke = false;
strokeColor = 0xff000000;
strokeWeight = 1;
strokeCap = PConstants.SQUARE; // equivalent to BUTT in svg spec
strokeJoin = PConstants.MITER;
strokeGradient = null;
strokeGradientPaint = null;
strokeName = null;
fill = true;
fillColor = 0xff000000;
fillGradient = null;
fillGradientPaint = null;
fillName = null;
//hasTransform = false;
//transformation = null; //new float[] { 1, 0, 0, 1, 0, 0 };
strokeOpacity = 255;
fillOpacity = 255;
opacity = 1;
} else {
stroke = parent.stroke;
strokeColor = parent.strokeColor;
strokeWeight = parent.strokeWeight;
strokeCap = parent.strokeCap;
strokeJoin = parent.strokeJoin;
strokeGradient = parent.strokeGradient;
strokeGradientPaint = parent.strokeGradientPaint;
strokeName = parent.strokeName;
fill = parent.fill;
fillColor = parent.fillColor;
fillGradient = parent.fillGradient;
fillGradientPaint = parent.fillGradientPaint;
fillName = parent.fillName;
//hasTransform = parent.hasTransform;
//transformation = parent.transformation;
strokeOpacity = parent.strokeOpacity;
fillOpacity = parent.fillOpacity;
opacity = parent.opacity;
}
element = properties;
id = properties.getStringAttribute("id");
if (id != null) {
table.put(id, this);
//System.out.println("now parsing " + id);
}
String displayStr = properties.getStringAttribute("display", "inline");
display = !displayStr.equals("none");
getColors(properties);
getTransformation(properties);
}
private void getTransformation(XMLElement properties) {
if (properties.hasAttribute("transform")) {
String transform = properties.getStringAttribute("transform");
setTransformation(transform);
}
}
protected void getColors(XMLElement properties) {
if (properties.hasAttribute("opacity")) {
String opacityText = properties.getStringAttribute("opacity");
setOpacity(opacityText);
}
if (properties.hasAttribute("stroke")) {
String strokeText = properties.getStringAttribute("stroke");
setStroke(strokeText);
}
if (properties.hasAttribute("stroke-width")) {
// if NaN (i.e. if it's 'inherit') then default back to the inherit setting
String lineweight = properties.getStringAttribute("stroke-width");
setStrokeWeight(lineweight);
}
if (properties.hasAttribute("stroke-linejoin")) {
String linejoin = properties.getStringAttribute("stroke-linejoin");
setStrokeJoin(linejoin);
}
if (properties.hasAttribute("stroke-linecap")) {
String linecap = properties.getStringAttribute("stroke-linecap");
setStrokeCap(linecap);
}
// fill defaults to black (though stroke defaults to "none")
// http://www.w3.org/TR/SVG/painting.html#FillProperties
if (properties.hasAttribute("fill")) {
String fillText = properties.getStringAttribute("fill");
setFill(fillText);
}
if (properties.hasAttribute("style")) {
String styleText = properties.getStringAttribute("style");
String[] styleTokens = PApplet.splitTokens(styleText, ";");
//PApplet.println(styleTokens);
for(int i = 0; i < styleTokens.length; i++){
String[] tokens = PApplet.splitTokens(styleTokens[i], ":");
//PApplet.println(tokens);
tokens[0] = PApplet.trim(tokens[0]);
if(tokens[0].equals("fill")){
setFill(tokens[1]);
}else if(tokens[0].equals("fill-opacity")){
setFillOpacity(tokens[1]);
}else if(tokens[0].equals("stroke")){
setStroke(tokens[1]);
}else if(tokens[0].equals("stroke-width")){
setStrokeWeight(tokens[1]);
}else if(tokens[0].equals("stroke-linecap")){
setStrokeCap(tokens[1]);
}else if(tokens[0].equals("stroke-linejoin")){
setStrokeJoin(tokens[1]);
}else if(tokens[0].equals("stroke-opacity")){
setStrokeOpacity(tokens[1]);
}else if(tokens[0].equals("opacity")){
setOpacity(tokens[1]);
}else{
// Other attributes are not yet implemented
}
}
}
}
void setTransformation(String transfText){
String[] transfTokens = PApplet.splitTokens(transfText, ")");
//if(transformation == null){
transformation = new PMatrix();
//}
hasTransform = true;
// Loop through all transformations
for(int i=0; i tag.");
}
}
}
public void drawShape() {
if (display) {
for (int i = 0; i < objectCount; i++) {
objects[i].draw();
}
}
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
abstract private class Gradient extends BaseObject {
AffineTransform transform;
float[] offset;
int[] color;
int count;
public Gradient(BaseObject parent, XMLElement properties) {
super(parent, properties);
XMLElement elements[] = properties.getChildren();
offset = new float[elements.length];
color = new int[elements.length];
//
for (int i = 0; i < elements.length; i++) {
XMLElement elem = elements[i];
String name = elem.getName();
if (name.equals("stop")) {
offset[count] = elem.getFloatAttribute("offset");
String style = elem.getStringAttribute("style");
Hashtable styles = parseStyleAttributes(style);
String colorStr = (String) styles.get("stop-color");
if (colorStr == null) colorStr = "#000000";
String opacityStr = (String) styles.get("stop-opacity");
if (opacityStr == null) opacityStr = "1";
int tupacity = (int) (PApplet.parseFloat(opacityStr) * 255);
color[count] = (tupacity << 24) |
Integer.parseInt(colorStr.substring(1), 16);
count++;
//System.out.println("this color is " + PApplet.hex(color[count]));
/*
int idx = farbe.indexOf("#");
if (idx != -1) {
color[count] = Integer.parseInt(farbe.substring(idx+1), 16);
count++;
} else {
System.err.println("problem with gradient stop " + properties);
}
*/
}
}
}
abstract protected void drawShape();
}
static protected Hashtable parseStyleAttributes(String style) {
Hashtable table = new Hashtable();
String[] pieces = style.split(";");
for (int i = 0; i < pieces.length; i++) {
String[] parts = pieces[i].split(":");
table.put(parts[0], parts[1]);
}
return table;
}
private class LinearGradient extends Gradient {
float x1, y1, x2, y2;
public LinearGradient(BaseObject parent, XMLElement properties) {
super(parent, properties);
this.x1 = properties.getFloatAttribute("x1");
this.y1 = properties.getFloatAttribute("y1");
this.x2 = properties.getFloatAttribute("x2");
this.y2 = properties.getFloatAttribute("y2");
String transformStr =
properties.getStringAttribute("gradientTransform");
if (transformStr != null) {
this.transform = parseTransform(transformStr);
Point2D t1 = transform.transform(new Point2D.Float(x1, y1), null);
Point2D t2 = transform.transform(new Point2D.Float(x2, y2), null);
this.x1 = (float) t1.getX();
this.y1 = (float) t1.getY();
this.x2 = (float) t2.getX();
this.y2 = (float) t2.getY();
}
}
protected void drawShape() {
}
}
// complete version is here
// http://www.w3.org/TR/SVG/coords.html#TransformAttribute
AffineTransform parseTransform(String what) {
if (what != null) {
if (what.startsWith("matrix(") && what.endsWith(")")) {
// columns go first with AT constructor
what = what.substring(7, what.length() - 1);
return new AffineTransform(PApplet.parseFloat(PApplet.split(what, ' ')));
}
}
return null;
}
private class RadialGradient extends Gradient {
float cx, cy, r;
public RadialGradient(BaseObject parent, XMLElement properties) {
super(parent, properties);
this.cx = properties.getFloatAttribute("cx");
this.cy = properties.getFloatAttribute("cy");
this.r = properties.getFloatAttribute("r");
String transformStr =
properties.getStringAttribute("gradientTransform");
if (transformStr != null) {
this.transform = parseTransform(transformStr);
Point2D t1 = transform.transform(new Point2D.Float(cx, cy), null);
Point2D t2 = transform.transform(new Point2D.Float(cx + r, cy), null);
this.cx = (float) t1.getX();
this.cy = (float) t1.getY();
this.r = (float) (t2.getX() - t1.getX());
}
}
protected void drawShape() {
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
private class Line extends BaseObject {
float x1, y1, x2, y2;
public Line(BaseObject parent, XMLElement properties) {
super(parent, properties);
this.x1 = properties.getFloatAttribute("x1");
this.y1 = properties.getFloatAttribute("y1");
this.x2 = properties.getFloatAttribute("x2");
this.y2 = properties.getFloatAttribute("y2");
}
protected void drawShape() {
parent.line(x1, y1, x2, y2);
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
private class Circle extends BaseObject {
float x, y, radius;
public Circle(BaseObject parent, XMLElement properties) {
super(parent, properties);
this.x = properties.getFloatAttribute("cx");
this.y = properties.getFloatAttribute("cy");
this.radius = properties.getFloatAttribute("r") * 2;
}
protected void drawShape() {
parent.ellipseMode(PConstants.CENTER);
parent.ellipse(x, y, radius, radius);
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
private class Ellipse extends BaseObject{
float x, y, rx, ry;
public Ellipse(BaseObject parent, XMLElement properties) {
super(parent, properties);
this.x = properties.getFloatAttribute("cx");
this.y = properties.getFloatAttribute("cy");
this.rx = properties.getFloatAttribute("rx") * 2;
this.ry = properties.getFloatAttribute("ry") * 2;
}
protected void drawShape() {
parent.ellipseMode(PConstants.CENTER);
parent.ellipse(x, y, rx, ry);
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
private class Rect extends BaseObject{
float x, y, w, h;
public Rect(BaseObject parent, XMLElement properties) {
super(parent, properties);
this.x = properties.getFloatAttribute("x");
this.y = properties.getFloatAttribute("y");
this.w = properties.getFloatAttribute("width");
this.h = properties.getFloatAttribute("height");
}
protected void drawShape() {
parent.rectMode(PConstants.CORNER);
parent.rect(x, y, w, h);
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
private class Poly extends BaseObject {
float points[][] = null;
/** true if polygon, false if polyline */
boolean closed;
public Poly(BaseObject parent, XMLElement properties, boolean closed) {
super(parent, properties);
String pointsBuffer[] = null;
this.closed = closed;
if (properties.hasAttribute("points")) {
pointsBuffer = PApplet.splitTokens(properties.getStringAttribute("points"));
}
points = new float[pointsBuffer.length][2];
for (int i = 0; i < points.length; i++) {
String pb[] = PApplet.split(pointsBuffer[i], ',');
points[i][0] = Float.valueOf(pb[0]).floatValue();
points[i][1] = Float.valueOf(pb[1]).floatValue();
}
}
protected void drawShape() {
if (points != null)
if (points.length > 0) {
parent.beginShape();
for (int i = 0; i < points.length; i++) {
parent.vertex(points[i][0], points[i][1]);
}
parent.endShape(closed ? PConstants.CLOSE : PConstants.OPEN);
}
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public class Path extends BaseObject {
public int count = 0;
public float[] x = new float[4];
public float[] y = new float[4];
static public final int MOVETO = 0;
static public final int LINETO = 1;
static public final int CURVETO = 2;
static public final int QCURVETO = 3;
public int[] kind = new int[4];
public boolean closed = false;
public Path(BaseObject parent, XMLElement properties) {
super(parent, properties);
String pathDataBuffer = "";
if (!properties.hasAttribute("d"))
return;
pathDataBuffer = properties.getStringAttribute("d");
StringBuffer pathChars = new StringBuffer();
boolean lastSeparate = false;
for (int i = 0; i < pathDataBuffer.length(); i++) {
char c = pathDataBuffer.charAt(i);
boolean separate = false;
if (c == 'M' || c == 'm' ||
c == 'L' || c == 'l' ||
c == 'H' || c == 'h' ||
c == 'V' || c == 'v' ||
c == 'C' || c == 'c' || // beziers
c == 'S' || c == 's' ||
c == 'Q' || c == 'q' || // quadratic beziers
c == 'T' || c == 't' ||
c == 'Z' || c == 'z' || // closepath
c == ',') {
separate = true;
if (i != 0) {
pathChars.append("|");
}
}
if (c == 'Z' || c == 'z') {
separate = false;
}
if (c == '-' && !lastSeparate) {
pathChars.append("|");
}
if (c != ',') {
pathChars.append("" + pathDataBuffer.charAt(i));
}
if (separate && c != ',' && c != '-') {
pathChars.append("|");
}
lastSeparate = separate;
}
pathDataBuffer = pathChars.toString();
//String pathDataKeys[] = PApplet.split(pathDataBuffer, '|');
// use whitespace constant to get rid of extra spaces and CR or LF
String pathDataKeys[] =
PApplet.splitTokens(pathDataBuffer, "|" + PConstants.WHITESPACE);
//for (int j = 0; j < pathDataKeys.length; j++) {
// PApplet.println(j + "\t" + pathDataKeys[j]);
//}
//PApplet.println(pathDataKeys);
//PApplet.println();
//float cp[] = {0, 0};
float cx = 0;
float cy = 0;
int i = 0;
//for (int i = 0; i < pathDataKeys.length; i++) {
while (i < pathDataKeys.length) {
char c = pathDataKeys[i].charAt(0);
switch (c) {
//M - move to (absolute)
case 'M':
/*
cp[0] = PApplet.parseFloat(pathDataKeys[i + 1]);
cp[1] = PApplet.parseFloat(pathDataKeys[i + 2]);
float s[] = {cp[0], cp[1]};
i += 2;
points.add(s);
*/
cx = PApplet.parseFloat(pathDataKeys[i + 1]);
cy = PApplet.parseFloat(pathDataKeys[i + 2]);
moveto(cx, cy);
i += 3;
break;
//m - move to (relative)
case 'm':
/*
cp[0] = cp[0] + PApplet.parseFloat(pathDataKeys[i + 1]);
cp[1] = cp[1] + PApplet.parseFloat(pathDataKeys[i + 2]);
float s[] = {cp[0], cp[1]};
i += 2;
points.add(s);
*/
cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
cy = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
moveto(cx, cy);
i += 3;
break;
case 'L':
cx = PApplet.parseFloat(pathDataKeys[i + 1]);
cy = PApplet.parseFloat(pathDataKeys[i + 2]);
lineto(cx, cy);
i += 3;
break;
case 'l':
cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
cy = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
lineto(cx, cy);
i += 3;
break;
// horizontal lineto absolute
case 'H':
cx = PApplet.parseFloat(pathDataKeys[i + 1]);
lineto(cx, cy);
i += 2;
break;
// horizontal lineto relative
case 'h':
cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
lineto(cx, cy);
i += 2;
break;
case 'V':
cy = PApplet.parseFloat(pathDataKeys[i + 1]);
lineto(cx, cy);
i += 2;
break;
case 'v':
cy = cy + PApplet.parseFloat(pathDataKeys[i + 1]);
lineto(cx, cy);
i += 2;
break;
// C - curve to (absolute)
case 'C': {
float ctrlX1 = PApplet.parseFloat(pathDataKeys[i + 1]);
float ctrlY1 = PApplet.parseFloat(pathDataKeys[i + 2]);
float ctrlX2 = PApplet.parseFloat(pathDataKeys[i + 3]);
float ctrlY2 = PApplet.parseFloat(pathDataKeys[i + 4]);
float endX = PApplet.parseFloat(pathDataKeys[i + 5]);
float endY = PApplet.parseFloat(pathDataKeys[i + 6]);
curveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY);
cx = endX;
cy = endY;
i += 7;
}
break;
// c - curve to (relative)
case 'c': {
float ctrlX1 = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
float ctrlY1 = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
float ctrlX2 = cx + PApplet.parseFloat(pathDataKeys[i + 3]);
float ctrlY2 = cy + PApplet.parseFloat(pathDataKeys[i + 4]);
float endX = cx + PApplet.parseFloat(pathDataKeys[i + 5]);
float endY = cy + PApplet.parseFloat(pathDataKeys[i + 6]);
curveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY);
cx = endX;
cy = endY;
i += 7;
}
break;
// S - curve to shorthand (absolute)
case 'S': {
float ppx = x[count-2];
float ppy = y[count-2];
float px = x[count-1];
float py = y[count-1];
float ctrlX1 = px + (px - ppx);
float ctrlY1 = py + (py - ppy);
float ctrlX2 = PApplet.parseFloat(pathDataKeys[i + 1]);
float ctrlY2 = PApplet.parseFloat(pathDataKeys[i + 2]);
float endX = PApplet.parseFloat(pathDataKeys[i + 3]);
float endY = PApplet.parseFloat(pathDataKeys[i + 4]);
curveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY);
cx = endX;
cy = endY;
i += 5;
}
break;
// s - curve to shorthand (relative)
case 's': {
float ppx = x[count-2];
float ppy = y[count-2];
float px = x[count-1];
float py = y[count-1];
float ctrlX1 = px + (px - ppx);
float ctrlY1 = py + (py - ppy);
float ctrlX2 = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
float ctrlY2 = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
float endX = cx + PApplet.parseFloat(pathDataKeys[i + 3]);
float endY = cy + PApplet.parseFloat(pathDataKeys[i + 4]);
curveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY);
cx = endX;
cy = endY;
i += 5;
}
break;
// Q - quadratic curve to (absolute)
case 'Q': {
float ctrlX = PApplet.parseFloat(pathDataKeys[i + 1]);
float ctrlY = PApplet.parseFloat(pathDataKeys[i + 2]);
float endX = PApplet.parseFloat(pathDataKeys[i + 3]);
float endY = PApplet.parseFloat(pathDataKeys[i + 4]);
curveto(ctrlX, ctrlY, endX, endY);
cx = endX;
cy = endY;
i += 5;
}
break;
// q - quadratic curve to (relative)
case 'q': {
float ctrlX = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
float ctrlY = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
float endX = cx + PApplet.parseFloat(pathDataKeys[i + 3]);
float endY = cy + PApplet.parseFloat(pathDataKeys[i + 4]);
curveto(ctrlX, ctrlY, endX, endY);
cx = endX;
cy = endY;
i += 5;
}
break;
// T - quadratic curve to shorthand (absolute)
// The control point is assumed to be the reflection of the
// control point on the previous command relative to the
// current point. (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.)
case 'T': {
float ppx = x[count-2];
float ppy = y[count-2];
float px = x[count-1];
float py = y[count-1];
float ctrlX = px + (px - ppx);
float ctrlY = py + (py - ppy);
float endX = PApplet.parseFloat(pathDataKeys[i + 1]);
float endY = PApplet.parseFloat(pathDataKeys[i + 2]);
curveto(ctrlX, ctrlY, endX, endY);
cx = endX;
cy = endY;
i += 3;
}
break;
// t - quadratic curve to shorthand (relative)
case 't': {
float ppx = x[count-2];
float ppy = y[count-2];
float px = x[count-1];
float py = y[count-1];
float ctrlX = px + (px - ppx);
float ctrlY = py + (py - ppy);
float endX = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
float endY = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
curveto(ctrlX, ctrlY, endX, endY);
cx = endX;
cy = endY;
i += 3;
}
break;
case 'Z':
case 'z':
closed = true;
i++;
break;
default:
String parsed =
PApplet.join(PApplet.subset(pathDataKeys, 0, i), ",");
String unparsed =
PApplet.join(PApplet.subset(pathDataKeys, i), ",");
System.err.println("parsed: " + parsed);
System.err.println("unparsed: " + unparsed);
throw new RuntimeException("shape command not handled: " + pathDataKeys[i]);
}
}
}
protected void moveto(float px, float py) {
if (count == x.length) {
x = PApplet.expand(x);
y = PApplet.expand(y);
kind = PApplet.expand(kind);
}
kind[count] = MOVETO;
x[count] = px;
y[count] = py;
count++;
}
protected void lineto(float px, float py) {
if (count == x.length) {
x = PApplet.expand(x);
y = PApplet.expand(y);
kind = PApplet.expand(kind);
}
kind[count] = LINETO;
x[count] = px;
y[count] = py;
count++;
}
/** Quadratic curveto command. */
protected void curveto(float x1, float y1, float x2, float y2) {
if (count + 2 >= x.length) {
x = PApplet.expand(x);
y = PApplet.expand(y);
kind = PApplet.expand(kind);
}
kind[count] = QCURVETO;
x[count] = x1;
y[count] = y1;
count++;
x[count] = x2;
y[count] = y2;
count++;
}
/** Cubic curveto command. */
protected void curveto(float x1, float y1, float x2, float y2, float x3, float y3) {
if (count + 2 >= x.length) {
x = PApplet.expand(x);
y = PApplet.expand(y);
kind = PApplet.expand(kind);
}
kind[count] = CURVETO;
x[count] = x1;
y[count] = y1;
count++;
x[count] = x2;
y[count] = y2;
count++;
x[count] = x3;
y[count] = y3;
count++;
}
protected void drawShape() {
parent.beginShape();
parent.vertex(x[0], y[0]);
int i = 1; // moveto has the first point
while (i < count) {
switch (kind[i]) {
case MOVETO:
parent.breakShape();
parent.vertex(x[i], y[i]);
i++;
break;
case LINETO:
parent.vertex(x[i], y[i]);
i++;
break;
case QCURVETO: // doubles the control point
parent.bezierVertex(x[i], y[i], x[i+1], y[i+1], x[i+1], y[i+1]);
i += 2;
break;
case CURVETO:
parent.bezierVertex(x[i], y[i], x[i+1], y[i+1], x[i+2], y[i+2]);
i += 3;
break;
}
}
parent.endShape(closed ? PConstants.CLOSE : PConstants.OPEN);
}
}
/*
public void draw() {
drawShape();
}
public void draw(PApplet tp) {
PApplet p = parent;
parent = tp;
drawShape();
parent = p;
}
public void draw(PGraphics parent) {
parent.beginShape();
parent.vertex(x[0], y[0]);
int i = 1; // moveto has the first point
while (i < count) {
switch (kind[i]) {
case MOVETO:
parent.breakShape();
parent.vertex(x[i], y[i]);
i++;
break;
case LINETO:
parent.vertex(x[i], y[i]);
i++;
break;
case QCURVETO: // doubles the control point
parent.bezierVertex(x[i], y[i], x[i+1], y[i+1], x[i+1], y[i+1]);
i += 2;
break;
case CURVETO:
parent.bezierVertex(x[i], y[i], x[i+1], y[i+1], x[i+2], y[i+2]);
i += 3;
break;
}
}
parent.endShape(closed ? PConstants.CLOSE : PConstants.OPEN);
}
*/
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/*
// http://www.w3.org/TR/SVG11/fonts.html
public class Font extends BaseObject {
// String id;
// XMLElement element;
public FontFace face;
public HashMap namedGlyphs;
public HashMap unicodeGlyphs;
public int glyphCount;
public FontGlyph[] glyphs;
public FontGlyph missingGlyph;
public Font(BaseObject parent, XMLElement properties) {
super(parent, properties);
XMLElement[] elements = properties.getChildren();
namedGlyphs = new HashMap();
unicodeGlyphs = new HashMap();
glyphCount = 0;
glyphs = new FontGlyph[elements.length];
for (int i = 0; i < elements.length; i++) {
String name = elements[i].getName();
XMLElement elem = elements[i];
if (name.equals("glyph")) {
FontGlyph fg = new FontGlyph(this, elem);
if (fg.count != 0) {
if (fg.name != null) {
namedGlyphs.put(fg.name, fg);
}
if (fg.unicode != 0) {
unicodeGlyphs.put(new Character(fg.unicode), fg);
}
}
glyphs[glyphCount++] = fg;
} else if (name.equals("missing-glyph")) {
missingGlyph = new FontGlyph(this, elem);
} else if (name.equals("font-face")) {
face = new FontFace(this, elem);
} else {
System.err.println("Ignoring " + name + " inside ");
}
}
}
protected void drawShape() {
// does nothing for fonts
}
public void drawString(String str, float x, float y, float size) {
// 1) scale by the 1.0/unitsPerEm
// 2) scale up by a font size
parent.pushMatrix();
float s = size / (float) face.unitsPerEm;
//System.out.println("scale is " + s);
// swap y coord at the same time, since fonts have y=0 at baseline
parent.translate(x, y);
parent.scale(s, -s);
char[] c = str.toCharArray();
for (int i = 0; i < c.length; i++) {
// call draw on each char (pulling it w/ the unicode table)
FontGlyph fg = (FontGlyph) unicodeGlyphs.get(new Character(c[i]));
if (fg != null) {
fg.draw();
// add horizAdvX/unitsPerEm to the x coordinate along the way
parent.translate(fg.horizAdvX, 0);
} else {
System.err.println("'" + c[i] + "' not available.");
}
}
parent.popMatrix();
}
public void drawChar(char c, float x, float y, float size) {
parent.pushMatrix();
float s = size / (float) face.unitsPerEm;
parent.translate(x, y);
parent.scale(s, -s);
FontGlyph fg = (FontGlyph) unicodeGlyphs.get(new Character(c));
if (fg != null) fg.draw();
parent.popMatrix();
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public class FontFace extends BaseObject {
int horizOriginX; // dflt 0
int horizOriginY; // dflt 0
int horizAdvX; // no dflt?
int vertOriginX; // dflt horizAdvX/2
int vertOriginY; // dflt ascent
int vertAdvY; // dflt 1em (unitsPerEm value)
String fontFamily;
int fontWeight; // can also be normal or bold (also comma separated)
String fontStretch;
int unitsPerEm; // dflt 1000
int[] panose1; // dflt "0 0 0 0 0 0 0 0 0 0"
int ascent;
int descent;
int[] bbox; // spec says comma separated, tho not w/ forge
int underlineThickness;
int underlinePosition;
//String unicodeRange; // gonna ignore for now
public FontFace(BaseObject parent, XMLElement properties) {
super(parent, properties);
unitsPerEm = properties.getIntAttribute("units-per-em", 1000);
}
protected void drawShape() {
// nothing to draw in the font face attribute
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public class FontGlyph extends Path {
String name;
//String unicode;
char unicode; //c;
int horizAdvX;
public FontGlyph(BaseObject parent, XMLElement properties) {
super(parent, properties);
name = properties.getStringAttribute("glyph-name");
String u = properties.getStringAttribute("unicode");
unicode = 0;
if (u != null) {
if (u.length() == 1) {
unicode = u.charAt(0);
//System.out.println("unicode for " + name + " is " + u);
} else {
System.err.println("unicode for " + name +
" is more than one char: " + u);
}
}
horizAdvX = properties.getIntAttribute("horiz-adv-x", 0);
}
}
*/
}