diff --git a/core/src/processing/awt/PShapeJava2D.java b/core/src/processing/awt/PShapeJava2D.java index 2b9b968a4c..4238ef924c 100644 --- a/core/src/processing/awt/PShapeJava2D.java +++ b/core/src/processing/awt/PShapeJava2D.java @@ -33,6 +33,9 @@ import java.awt.image.Raster; import java.awt.image.WritableRaster; +import java.util.Arrays; +import java.awt.Color; + import processing.core.PApplet; import processing.core.PGraphics; import processing.core.PShapeSVG; @@ -96,16 +99,29 @@ public void setColor(String colorText, boolean isFill) { */ - static class LinearGradientPaint implements Paint { + public static class LinearGradientPaint implements Paint { float x1, y1, x2, y2; float[] offset; int[] color; + Color[] colors; int count; float opacity; + AffineTransform xform; + + public static enum CycleMethod { + NO_CYCLE, + REFLECT, + REPEAT + } + + public static enum ColorSpaceType { + SRGB, + LINEAR_RGB + } public LinearGradientPaint(float x1, float y1, float x2, float y2, float[] offset, int[] color, int count, - float opacity) { + float opacity, AffineTransform xform) { this.x1 = x1; this.y1 = y1; this.x2 = x2; @@ -114,6 +130,13 @@ public LinearGradientPaint(float x1, float y1, float x2, float y2, this.color = color; this.count = count; this.opacity = opacity; + this.xform = xform; + + //set an array of type Color + this.colors = new Color[this.color.length]; + for (int i = 0; i < this.color.length; i++) { + this.colors[i] = new Color(this.color[i], true); + } } public PaintContext createContext(ColorModel cm, @@ -125,6 +148,35 @@ public PaintContext createContext(ColorModel cm, (float) t2.getX(), (float) t2.getY()); } + public Point2D getStartPoint() { + return new Point2D.Float(this.x1, this.y1); + } + + public Point2D getEndPoint() { + return new Point2D.Float(this.x2, this.y2); + } + + /* MultipleGradientPaint methods... */ + public AffineTransform getTransform() { + return this.xform; + } + + public ColorSpaceType getColorSpace() { + return ColorSpaceType.SRGB; + } + + public CycleMethod getCycleMethod() { + return CycleMethod.NO_CYCLE; + } + + public Color[] getColors() { + return Arrays.copyOf(this.colors, this.colors.length); + } + + public float[] getFractions() { + return Arrays.copyOf(this.offset, this.offset.length); + } + public int getTransparency() { return TRANSLUCENT; // why not.. rather than checking each color } @@ -221,16 +273,29 @@ public Raster getRaster(int x, int y, int w, int h) { } - static class RadialGradientPaint implements Paint { + public static class RadialGradientPaint implements Paint { float cx, cy, radius; float[] offset; int[] color; + Color[] colors; int count; float opacity; + AffineTransform xform; + + public static enum CycleMethod { + NO_CYCLE, + REFLECT, + REPEAT + } + + public static enum ColorSpaceType { + SRGB, + LINEAR_RGB + } public RadialGradientPaint(float cx, float cy, float radius, float[] offset, int[] color, int count, - float opacity) { + float opacity, AffineTransform xform) { this.cx = cx; this.cy = cy; this.radius = radius; @@ -238,6 +303,13 @@ public RadialGradientPaint(float cx, float cy, float radius, this.color = color; this.count = count; this.opacity = opacity; + this.xform = xform; + + //set an array of type Color + this.colors = new Color[this.color.length]; + for (int i = 0; i < this.color.length; i++) { + this.colors[i] = new Color(this.color[i], true); + } } public PaintContext createContext(ColorModel cm, @@ -246,6 +318,41 @@ public PaintContext createContext(ColorModel cm, return new RadialGradientContext(); } + public Point2D getCenterPoint() { + return new Point2D.Double(this.cx, this.cy); + } + + //TODO: investigate how to change a focus point for 0% x of the gradient + //for now default to center x/y + public Point2D getFocusPoint() { + return new Point2D.Double(this.cx, this.cy); + } + + public float getRadius() { + return this.radius; + } + + /* MultipleGradientPaint methods... */ + public AffineTransform getTransform() { + return this.xform; + } + + public ColorSpaceType getColorSpace() { + return ColorSpaceType.SRGB; + } + + public CycleMethod getCycleMethod() { + return CycleMethod.NO_CYCLE; + } + + public Color[] getColors() { + return Arrays.copyOf(this.colors, this.colors.length); + } + + public float[] getFractions() { + return Arrays.copyOf(this.offset, this.offset.length); + } + public int getTransparency() { return TRANSLUCENT; } @@ -305,14 +412,14 @@ protected Paint calcGradientPaint(Gradient gradient) { LinearGradient grad = (LinearGradient) gradient; return new LinearGradientPaint(grad.x1, grad.y1, grad.x2, grad.y2, grad.offset, grad.color, grad.count, - opacity); + opacity, grad.transform); } else if (gradient instanceof RadialGradient) { // System.out.println("creating radial gradient"); RadialGradient grad = (RadialGradient) gradient; return new RadialGradientPaint(grad.cx, grad.cy, grad.r, grad.offset, grad.color, grad.count, - opacity); + opacity, grad.transform); } return null; } diff --git a/core/src/processing/core/PShapeSVG.java b/core/src/processing/core/PShapeSVG.java index a51e94276b..f8aa3400fb 100644 --- a/core/src/processing/core/PShapeSVG.java +++ b/core/src/processing/core/PShapeSVG.java @@ -1397,7 +1397,8 @@ void setFillOpacity(String opacityText) { } - void setColor(String colorText, boolean isFill) { + //making this public allows us to set gradient fills on a PShape + public void setColor(String colorText, boolean isFill) { colorText = colorText.trim(); int opacityMask = fillColor & 0xFF000000; boolean visible = true; @@ -1620,7 +1621,7 @@ static protected float parseFloatOrPercent(String text) { static public class Gradient extends PShapeSVG { - AffineTransform transform; + public AffineTransform transform; public float[] offset; public int[] color; diff --git a/java/libraries/svg/src/processing/svg/GradientExtensionHandler.java b/java/libraries/svg/src/processing/svg/GradientExtensionHandler.java new file mode 100644 index 0000000000..370adbea93 --- /dev/null +++ b/java/libraries/svg/src/processing/svg/GradientExtensionHandler.java @@ -0,0 +1,209 @@ +package processing.svg; + +import static org.apache.batik.util.SVGConstants.*; + +import processing.awt.PShapeJava2D.LinearGradientPaint; +import processing.awt.PShapeJava2D.RadialGradientPaint; + +import java.awt.Color; +import java.awt.MultipleGradientPaint; +import java.awt.Paint; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; + +import java.util.Objects; + +import org.apache.batik.svggen.DefaultExtensionHandler; +import org.apache.batik.svggen.SVGColor; +import org.apache.batik.svggen.SVGGeneratorContext; +import org.apache.batik.svggen.SVGPaintDescriptor; +import org.w3c.dom.Element; + +/** + * Extension of Batik's {@link DefaultExtensionHandler} which handles different kinds of Paint objects + * based on the extenstion by Martin Steiger https://gist.github.com/msteiger/4509119 + * modified to work with Processing's SVG export library, by Benjamin Fox https://github.com/tracerstar + */ +public class GradientExtensionHandler extends DefaultExtensionHandler { + + @Override + public SVGPaintDescriptor handlePaint(Paint paint, SVGGeneratorContext genCtx) { + + // Handle LinearGradientPaint + if (paint instanceof LinearGradientPaint) { + return getLgpDescriptor((LinearGradientPaint) paint, genCtx); + } + + // Handle RadialGradientPaint + if (paint instanceof RadialGradientPaint) { + return getRgpDescriptor((RadialGradientPaint) paint, genCtx); + } + + return super.handlePaint(paint, genCtx); + } + + private SVGPaintDescriptor getLgpDescriptor(LinearGradientPaint gradient, SVGGeneratorContext genCtx) { + Element gradElem = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_LINEAR_GRADIENT_TAG); + + // Create and set unique XML id + String id = genCtx.getIDGenerator().generateID("gradient"); + gradElem.setAttribute(SVG_ID_ATTRIBUTE, id); + + // Set x,y pairs + Point2D startPt = gradient.getStartPoint(); + gradElem.setAttribute("x1", String.valueOf(startPt.getX())); + gradElem.setAttribute("y1", String.valueOf(startPt.getY())); + + Point2D endPt = gradient.getEndPoint(); + gradElem.setAttribute("x2", String.valueOf(endPt.getX())); + gradElem.setAttribute("y2", String.valueOf(endPt.getY())); + + //TODO: change this to be: addMgpAttributes after refactoring the paint methods + addLgpAttributes(gradElem, genCtx, gradient); + + return new SVGPaintDescriptor("url(#" + id + ")", SVG_OPAQUE_VALUE, gradElem); + } + + private SVGPaintDescriptor getRgpDescriptor(RadialGradientPaint gradient, SVGGeneratorContext genCtx) { + Element gradElem = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_RADIAL_GRADIENT_TAG); + + // Create and set unique XML id + String id = genCtx.getIDGenerator().generateID("gradient"); + gradElem.setAttribute(SVG_ID_ATTRIBUTE, id); + + // Set x,y pairs + Point2D centerPt = gradient.getCenterPoint(); + gradElem.setAttribute("cx", String.valueOf(centerPt.getX())); + gradElem.setAttribute("cy", String.valueOf(centerPt.getY())); + + Point2D focusPt = gradient.getFocusPoint(); + gradElem.setAttribute("fx", String.valueOf(focusPt.getX())); + gradElem.setAttribute("fy", String.valueOf(focusPt.getY())); + + gradElem.setAttribute("r", String.valueOf(gradient.getRadius())); + + //TODO: change this to be: addMgpAttributes after refactoring the paint methods + addRgpAttributes(gradElem, genCtx, gradient); + + return new SVGPaintDescriptor("url(#" + id + ")", SVG_OPAQUE_VALUE, gradElem); + } + + + /* + Being lazy here to duplicate the methods so we don't have to refactor the two gradient paints + to implement java.awt.MultipleGradientPaint + + TODO: make the effort to refactor them to properly implement java.awt.MultipleGradientPaint + */ + private void addLgpAttributes(Element gradElem, SVGGeneratorContext genCtx, LinearGradientPaint gradient) { + gradElem.setAttribute(SVG_GRADIENT_UNITS_ATTRIBUTE, SVG_USER_SPACE_ON_USE_VALUE); + + // Set cycle method + switch (gradient.getCycleMethod()) { + case REFLECT: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REFLECT_VALUE); + break; + case REPEAT: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REPEAT_VALUE); + break; + case NO_CYCLE: + default: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_PAD_VALUE); // this is the default + break; + } + + // Set color space + switch (gradient.getColorSpace()) { + case LINEAR_RGB: + gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_LINEAR_RGB_VALUE); + break; + case SRGB: + default: + gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_SRGB_VALUE); + break; + } + + // Set transform matrix if not identity + AffineTransform tf = gradient.getTransform(); + if (!Objects.isNull(tf) && !tf.isIdentity()) { + String matrix = "matrix(" + + tf.getScaleX() + " " + tf.getShearX() + " " + tf.getTranslateX() + " " + + tf.getScaleY() + " " + tf.getShearY() + " " + tf.getTranslateY() + ")"; + gradElem.setAttribute(SVG_TRANSFORM_ATTRIBUTE, matrix); + } + + // Convert gradient stops + Color[] colors = gradient.getColors(); + float[] fracs = gradient.getFractions(); + + for (int i = 0; i < colors.length; i++) { + Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG); + SVGPaintDescriptor pd = SVGColor.toSVG(colors[i], genCtx); + + stop.setAttribute(SVG_OFFSET_ATTRIBUTE, (int) (fracs[i] * 100.0f) + "%"); + stop.setAttribute(SVG_STOP_COLOR_ATTRIBUTE, pd.getPaintValue()); + + if (colors[i].getAlpha() != 255) { + stop.setAttribute(SVG_STOP_OPACITY_ATTRIBUTE, pd.getOpacityValue()); + } + + gradElem.appendChild(stop); + } + } + + private void addRgpAttributes(Element gradElem, SVGGeneratorContext genCtx, RadialGradientPaint gradient) { + gradElem.setAttribute(SVG_GRADIENT_UNITS_ATTRIBUTE, SVG_USER_SPACE_ON_USE_VALUE); + + // Set cycle method + switch (gradient.getCycleMethod()) { + case REFLECT: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REFLECT_VALUE); + break; + case REPEAT: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REPEAT_VALUE); + break; + case NO_CYCLE: + default: + gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_PAD_VALUE); // this is the default + break; + } + + // Set color space + switch (gradient.getColorSpace()) { + case LINEAR_RGB: + gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_LINEAR_RGB_VALUE); + break; + case SRGB: + default: + gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_SRGB_VALUE); + break; + } + + // Set transform matrix if not identity + AffineTransform tf = gradient.getTransform(); + if (!Objects.isNull(tf) && !tf.isIdentity()) { + String matrix = "matrix(" + + tf.getScaleX() + " " + tf.getShearX() + " " + tf.getTranslateX() + " " + + tf.getScaleY() + " " + tf.getShearY() + " " + tf.getTranslateY() + ")"; + gradElem.setAttribute(SVG_TRANSFORM_ATTRIBUTE, matrix); + } + + // Convert gradient stops + Color[] colors = gradient.getColors(); + float[] fracs = gradient.getFractions(); + + for (int i = 0; i < colors.length; i++) { + Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG); + SVGPaintDescriptor pd = SVGColor.toSVG(colors[i], genCtx); + + stop.setAttribute(SVG_OFFSET_ATTRIBUTE, (int) (fracs[i] * 100.0f) + "%"); + stop.setAttribute(SVG_STOP_COLOR_ATTRIBUTE, pd.getPaintValue()); + + if (colors[i].getAlpha() != 255) { + stop.setAttribute(SVG_STOP_OPACITY_ATTRIBUTE, pd.getOpacityValue()); + } + + gradElem.appendChild(stop); + } + } +} diff --git a/java/libraries/svg/src/processing/svg/PGraphicsSVG.java b/java/libraries/svg/src/processing/svg/PGraphicsSVG.java index 3b170353ba..857f5d93a3 100644 --- a/java/libraries/svg/src/processing/svg/PGraphicsSVG.java +++ b/java/libraries/svg/src/processing/svg/PGraphicsSVG.java @@ -87,6 +87,10 @@ public void beginDraw() { g2 = new SVGGraphics2D(document); ((SVGGraphics2D) g2).setSVGCanvasSize(new Dimension(width, height)); + //set the extension handler to allow linear and radial gradients to be exported as svg + GradientExtensionHandler gradH = new GradientExtensionHandler(); + ((SVGGraphics2D) g2).setExtensionHandler(gradH); + // Done with our work, let's check on defaults and the rest //super.beginDraw(); // Can't call super.beginDraw() because it'll nuke our g2