Java tutorial
/* =================================================== * JFreeSVG : an SVG library for the Java(tm) platform * =================================================== * * (C)opyright 2013, 2014, by Object Refinery Limited. All rights reserved. * * Project Info: http://www.jfree.org/jfreesvg/index.html * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * If you do not wish to be bound by the terms of the GPL, an alternative * commercial license can be purchased. For details, please see visit the * JFreeSVG home page: * * http://www.jfree.org/jfreesvg * */ package org.jfree.graphics2d.canvas; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.Image; import java.awt.Paint; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ImageObserver; import java.awt.image.RenderedImage; import java.awt.image.renderable.RenderableImage; import java.text.AttributedCharacterIterator; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Map; import org.jfree.graphics2d.Args; import org.jfree.graphics2d.GraphicsUtils; /** * A <code>Graphics2D</code> implementation that writes out JavaScript code * that will draw to an HTML5 Canvas. * <p> * Implementation notes: * <ul> * <li>all rendering hints are ignored;</li> * <li>images are not yet supported;</li> * <li>the <code>drawString()</code> methods that work with an * <code>AttributedCharacterIterator</code> currently ignore the formatting * information.</li> * </ul> * <p> * For some demos of the use of this class, please look in the * <code>org.jfree.graphics2d.demo</code> package in the <code>src</code> * directory. */ public final class CanvasGraphics2D extends Graphics2D { /** The canvas ID. */ private String canvasID; /** The buffer for all the Javascript output. */ private StringBuilder sb; /** Rendering hints (all ignored). */ private RenderingHints hints; private Shape clip;; private Paint paint = Color.BLACK; private Color color = Color.BLACK; private Composite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f); private Stroke stroke = new BasicStroke(1.0f); private Font font = new Font("SansSerif", Font.PLAIN, 12); private AffineTransform transform = new AffineTransform(); /** The background color, presently ignored. */ private Color background = Color.BLACK; /** A hidden image used for font metrics. */ private BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);; /** * An instance that is lazily instantiated in drawLine and then * subsequently reused to avoid creating a lot of garbage. */ private Line2D line; /** * An instance that is lazily instantiated in fillRect and then * subsequently reused to avoid creating a lot of garbage. */ Rectangle2D rect; /** * An instance that is lazily instantiated in draw/fillRoundRect and then * subsequently reused to avoid creating a lot of garbage. */ private RoundRectangle2D roundRect; /** * An instance that is lazily instantiated in draw/fillOval and then * subsequently reused to avoid creating a lot of garbage. */ private Ellipse2D oval; /** * An instance that is lazily instantiated in draw/fillArc and then * subsequently reused to avoid creating a lot of garbage. */ private Arc2D arc; /** * The number of decimal places to use when writing the matrix values * for transformations. */ private int transformDP; /** * The decimal formatter for transform matrices. */ private DecimalFormat transformFormat; /** * The number of decimal places to use when writing coordinates for * geometrical shapes. */ private int geometryDP; /** * The decimal formatter for coordinates of geometrical shapes. */ private DecimalFormat geometryFormat = new DecimalFormat("0.##"); /** * Creates a new instance. The canvas ID is stored but not used in the * current implementation. * * @param canvasID the canvas ID. */ public CanvasGraphics2D(String canvasID) { this.canvasID = canvasID; this.sb = new StringBuilder(); this.hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); this.clip = null; DecimalFormatSymbols dfs = new DecimalFormatSymbols(); dfs.setDecimalSeparator('.'); this.transformFormat = new DecimalFormat("0.######", dfs); this.geometryFormat = new DecimalFormat("0.##", dfs); } /** * Returns the canvas ID that was passed to the constructor. * * @return The canvas ID. */ public String getCanvasID() { return this.canvasID; } /** * Returns the number of decimal places used to write the transformation * matrices in the Javascript output. The default value is 6. * <p> * Note that there is a separate attribute to control the number of decimal * places for geometrical elements in the output (see * {@link #getGeometryDP()}). * * @return The number of decimal places. * * @see #setTransformDP(int) */ public int getTransformDP() { return this.transformDP; } /** * Sets the number of decimal places used to write the transformation * matrices in the Javascript output. Values in the range 1 to 10 will be * used to configure a formatter to that number of decimal places, for all * other values we revert to the normal <code>String</code> conversion of * <code>double</code> primitives (approximately 16 decimals places). * <p> * Note that there is a separate attribute to control the number of decimal * places for geometrical elements in the output (see * {@link #setGeometryDP(int)}). * * @param dp the number of decimal places (normally 1 to 10). * * @see #getTransformDP() */ public void setTransformDP(int dp) { this.transformDP = dp; if (dp < 1 || dp > 10) { this.transformFormat = null; return; } DecimalFormatSymbols dfs = new DecimalFormatSymbols(); dfs.setDecimalSeparator('.'); this.transformFormat = new DecimalFormat("0." + "##########".substring(0, dp), dfs); } /** * Returns the number of decimal places used to write the coordinates * of geometrical shapes. The default value is 2. * <p> * Note that there is a separate attribute to control the number of decimal * places for transform matrices in the output (see * {@link #getTransformDP()}). * * @return The number of decimal places. */ public int getGeometryDP() { return this.geometryDP; } /** * Sets the number of decimal places used to write the coordinates of * geometrical shapes in the Javascript output. Values in the range 1 to 10 * will be used to configure a formatter to that number of decimal places, * for all other values we revert to the normal String conversion of double * primitives (approximately 16 decimals places). * <p> * Note that there is a separate attribute to control the number of decimal * places for transform matrices in the output (see * {@link #setTransformDP(int)}). * * @param dp the number of decimal places (normally 1 to 10). */ public void setGeometryDP(int dp) { this.geometryDP = dp; if (dp < 1 || dp > 10) { this.geometryFormat = null; return; } DecimalFormatSymbols dfs = new DecimalFormatSymbols(); dfs.setDecimalSeparator('.'); this.geometryFormat = new DecimalFormat("0." + "##########".substring(0, dp), dfs); } /** * Not yet implemented. * * @return The graphics configuration. */ @Override public GraphicsConfiguration getDeviceConfiguration() { throw new UnsupportedOperationException("Not supported yet."); //TODO } /** * Creates a new graphics object that is a copy of this graphics object. * * @return A new graphics object. */ @Override public Graphics create() { CanvasGraphics2D copy = new CanvasGraphics2D(this.canvasID); copy.setRenderingHints(getRenderingHints()); copy.setClip(getClip()); copy.setPaint(getPaint()); copy.setColor(getColor()); copy.setComposite(getComposite()); copy.setStroke(getStroke()); copy.setFont(getFont()); copy.setTransform(getTransform()); copy.setBackground(getBackground()); return copy; } /** * Returns the paint used to draw or fill shapes (or text). The default * value is {@link Color#BLACK}. * * @return The paint (never <code>null</code>). * * @see #setPaint(java.awt.Paint) */ @Override public Paint getPaint() { return this.paint; } /** * Sets the paint used to draw or fill shapes (or text). If * <code>paint</code> is an instance of <code>Color</code>, this method will * also update the current color attribute (see {@link #getColor()}). If * you pass <code>null</code> to this method, it does nothing (in * accordance with the JDK specification). * * @param paint the paint (<code>null</code> is permitted but ignored). * * @see #getPaint() */ @Override public void setPaint(Paint paint) { if (paint == null) { return; } this.paint = paint; if (paint instanceof Color) { setColor((Color) paint); } else if (paint instanceof GradientPaint) { GradientPaint gp = (GradientPaint) paint; Point2D p1 = gp.getPoint1(); Point2D p2 = gp.getPoint2(); this.sb.append("var g = ctx.createLinearGradient(").append(geomDP(p1.getX())).append(",") .append(geomDP(p1.getY())).append(",").append(geomDP(p2.getX())).append(",") .append(geomDP(p2.getY())).append(");"); this.sb.append("g.addColorStop(0,'").append(toCSSColorValue(gp.getColor1())).append("');"); this.sb.append("g.addColorStop(1,'").append(toCSSColorValue(gp.getColor2())).append("');"); this.sb.append("ctx.fillStyle=g;"); } else { System.err.println("setPaint(" + paint + ")"); } } /** * Returns the foreground color. This method exists for backwards * compatibility in AWT, you should use the {@link #getPaint()} method. * * @return The foreground color (never <code>null</code>). * * @see #getPaint() */ @Override public Color getColor() { return this.color; } /** * Sets the foreground color. This method exists for backwards * compatibility in AWT, you should use the * {@link #setPaint(java.awt.Paint)} method. * * @param c the color (<code>null</code> permitted but ignored). * * @see #setPaint(java.awt.Paint) */ @Override public void setColor(Color c) { if (c == null || this.color.equals(c)) { return; // nothing to do } this.color = c; this.paint = c; String cssColor = toCSSColorValue(c); // TODO: we could avoid writing both of these by tracking dirty // flags and only writing the appropriate style when required this.sb.append("ctx.fillStyle=\"").append(cssColor).append("\";"); this.sb.append("ctx.strokeStyle=\"").append(cssColor).append("\";"); } /** * A utility method that translates a Color object to a CSS color string. * * @param c the color (<code>null</code> not permitted). * * @return The CSS string for the color specification. */ private String toCSSColorValue(Color c) { return "rgba(" + c.getRed() + "," + c.getGreen() + "," + c.getBlue() + "," + c.getAlpha() / 255.0f + ")"; } /** * Returns the background color. The default value is {@link Color#BLACK}. * This is used by the {@link #clearRect(int, int, int, int)} method. * * @return The background color (possibly <code>null</code>). * * @see #setBackground(java.awt.Color) */ @Override public Color getBackground() { return this.background; } /** * Sets the background color. This is used by the * {@link #clearRect(int, int, int, int)} method. The reference * implementation allows <code>null</code> for the background color so * we allow that too (but for that case, the clearRect method will do * nothing). * * @param color the color (<code>null</code> permitted). * * @see #getBackground() */ @Override public void setBackground(Color color) { this.background = color; } /** * Returns the current composite. * * @return The current composite (never <code>null</code>). * * @see #setComposite(java.awt.Composite) */ @Override public Composite getComposite() { return this.composite; } /** * Sets the composite (only <code>AlphaComposite</code> is handled). * * @param comp the composite (<code>null</code> not permitted). * * @see #getComposite() */ @Override public void setComposite(Composite comp) { Args.nullNotPermitted(comp, "comp"); this.composite = comp; if (comp instanceof AlphaComposite) { AlphaComposite ac = (AlphaComposite) comp; sb.append("ctx.globalAlpha=").append(ac.getAlpha()).append(";"); sb.append("ctx.globalCompositeOperation=\"").append(toJSCompositeRuleName(ac.getRule())).append("\";"); } } private String toJSCompositeRuleName(int rule) { switch (rule) { case AlphaComposite.CLEAR: return "xor"; case AlphaComposite.SRC_IN: return "source-in"; case AlphaComposite.SRC_OUT: return "source-out"; case AlphaComposite.SRC_OVER: return "source-over"; case AlphaComposite.SRC_ATOP: return "source-atop"; case AlphaComposite.DST_IN: return "destination-in"; case AlphaComposite.DST_OUT: return "destination-out"; case AlphaComposite.DST_OVER: return "destination-over"; case AlphaComposite.DST_ATOP: return "destination-atop"; default: throw new IllegalArgumentException("Unknown/unhandled 'rule' " + rule); } } /** * Returns the current stroke (used when drawing shapes). * * @return The current stroke (never <code>null</code>). * * @see #setStroke(java.awt.Stroke) */ @Override public Stroke getStroke() { return this.stroke; } /** * Sets the stroke that will be used to draw shapes. Only * <code>BasicStroke</code> is supported. * * @param s the stroke (<code>null</code> not permitted). * * @see #getStroke() */ @Override public void setStroke(Stroke s) { Args.nullNotPermitted(s, "s"); this.stroke = s; if (s instanceof BasicStroke) { BasicStroke bs = (BasicStroke) s; sb.append("ctx.lineWidth=").append(bs.getLineWidth()).append(";"); if (bs.getDashArray() != null) { sb.append("ctx.setLineDash(["); for (int i = 0; i < bs.getDashArray().length; i++) { if (i != 0) { sb.append(","); } sb.append((int) bs.getDashArray()[i]); } sb.append("]);"); } else { sb.append("ctx.setLineDash([]);"); } } } /** * Returns the current value for the specified hint. Note that all hints * are currently ignored in this implementation. * * @param hintKey the hint key (<code>null</code> permitted, but the * result will be <code>null</code> also). * * @return The current value for the specified hint * (possibly <code>null</code>). * * @see #setRenderingHint(java.awt.RenderingHints.Key, java.lang.Object) */ @Override public Object getRenderingHint(RenderingHints.Key hintKey) { return this.hints.get(hintKey); } /** * Sets the value for a hint. Note that all hints are currently * ignored in this implementation. * * @param hintKey the hint key (<code>null</code> not permitted). * @param hintValue the hint value. * * @see #getRenderingHint(java.awt.RenderingHints.Key) */ @Override public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { this.hints.put(hintKey, hintValue); } /** * Returns a copy of the rendering hints. Modifying the returned copy * will have no impact on the state of this Graphics2D instance. * * @return The rendering hints (never <code>null</code>). * * @see #setRenderingHints(java.util.Map) */ @Override public RenderingHints getRenderingHints() { return (RenderingHints) this.hints.clone(); } /** * Sets the rendering hints to the specified collection. * * @param hints the new set of hints (<code>null</code> not permitted). * * @see #getRenderingHints() */ @Override public void setRenderingHints(Map<?, ?> hints) { this.hints.clear(); this.hints.putAll(hints); } /** * Adds all the supplied rendering hints. * * @param hints the hints (<code>null</code> not permitted). */ @Override public void addRenderingHints(Map<?, ?> hints) { this.hints.putAll(hints); } /** * Draws the specified shape with the current <code>paint</code> and * <code>stroke</code>. There is direct handling for <code>Line2D</code>, * <code>Rectangle2D</code> and <code>Path2D</code>. All other shapes are * mapped to a <code>GeneralPath</code> and then drawn (effectively as * <code>Path2D</code> objects). * * @param s the shape (<code>null</code> not permitted). * * @see #fill(java.awt.Shape) */ @Override public void draw(Shape s) { if (s instanceof Line2D || s instanceof Rectangle2D || s instanceof Path2D) { shapeToPath(s); sb.append("ctx.stroke();"); } else { draw(new GeneralPath(s)); } } /** * Fills the specified shape with the current <code>paint</code>. There is * direct handling for <code>Rectangle2D</code> and <code>Path2D</code>. * All other shapes are mapped to a <code>GeneralPath</code> and then * filled. * * @param s the shape (<code>null</code> not permitted). * * @see #draw(java.awt.Shape) */ @Override public void fill(Shape s) { if (s instanceof Rectangle2D) { Rectangle2D r = (Rectangle2D) s; if (r.isEmpty()) { return; } sb.append("ctx.fillRect(").append(geomDP(r.getX())).append(",").append(geomDP(r.getY())).append(",") .append(geomDP(r.getWidth())).append(",").append(geomDP(r.getHeight())).append(");"); } else if (s instanceof Path2D) { shapeToPath(s); sb.append("ctx.fill();"); } else { fill(new GeneralPath(s)); } } private void shapeToPath(Shape s) { if (s instanceof Line2D) { Line2D l = (Line2D) s; sb.append("ctx.beginPath();"); sb.append("ctx.moveTo(").append(geomDP(l.getX1())).append(",").append(geomDP(l.getY1())).append(");"); sb.append("ctx.lineTo(").append(geomDP(l.getX2())).append(",").append(geomDP(l.getY2())).append(");"); sb.append("ctx.closePath();"); } else if (s instanceof Rectangle2D) { Rectangle2D r = (Rectangle2D) s; sb.append("ctx.beginPath();"); sb.append("ctx.rect(").append(geomDP(r.getX())).append(",").append(geomDP(r.getY())).append(",") .append(geomDP(r.getWidth())).append(",").append(geomDP(r.getHeight())).append(");"); sb.append("ctx.closePath();"); } else if (s instanceof Path2D) { Path2D p = (Path2D) s; float[] coords = new float[6]; double[] closePt = null; PathIterator iterator = p.getPathIterator(getTransform()); sb.append("ctx.beginPath();"); while (!iterator.isDone()) { int type = iterator.currentSegment(coords); switch (type) { case (PathIterator.SEG_MOVETO): closePt = new double[2]; closePt[0] = coords[0]; closePt[1] = coords[1]; sb.append("ctx.moveTo(").append(geomDP(coords[0])).append(",").append(geomDP(coords[1])) .append(");"); break; case (PathIterator.SEG_LINETO): sb.append("ctx.lineTo(").append(geomDP(coords[0])).append(",").append(geomDP(coords[1])) .append(");"); break; case (PathIterator.SEG_QUADTO): sb.append("ctx.quadraticCurveTo(").append(geomDP(coords[0])).append(",") .append(geomDP(coords[1])).append(",").append(geomDP(coords[2])).append(",") .append(geomDP(coords[3])).append(");"); break; case (PathIterator.SEG_CUBICTO): sb.append("ctx.bezierCurveTo(").append(geomDP(coords[0])).append(",").append(geomDP(coords[1])) .append(",").append(geomDP(coords[2])).append(",").append(geomDP(coords[3])).append(",") .append(geomDP(coords[4])).append(",").append(geomDP(coords[5])).append(");"); break; case (PathIterator.SEG_CLOSE): if (closePt != null) { sb.append("ctx.lineTo(").append(geomDP(closePt[0])).append(",").append(geomDP(closePt[1])) .append(");"); } break; default: break; } iterator.next(); } //sb.append("ctx.closePath();"); } else { throw new RuntimeException("Unhandled shape " + s); } } /** * Returns the current font used for drawing text. * * @return The current font (never <code>null</code>). * * @see #setFont(java.awt.Font) */ @Override public Font getFont() { return this.font; } /** * Sets the font to be used for drawing text. * * @param font the font (<code>null</code> is permitted but ignored). * * @see #getFont() */ @Override public void setFont(Font font) { if (font == null || this.font.equals(font)) { return; } this.font = font; this.sb.append("ctx.font=\"").append(font.getSize()).append("px ").append(font.getFontName()).append("\";"); } /** * Returns the font metrics for the specified font. * * @param f the font. * * @return The font metrics. */ @Override public FontMetrics getFontMetrics(Font f) { return this.image.createGraphics().getFontMetrics(f); } /** * Returns the font render context. The implementation here returns the * <code>FontRenderContext</code> for an image that is maintained * internally (as for {@link #getFontMetrics}). * * @return The font render context. */ @Override public FontRenderContext getFontRenderContext() { return this.image.createGraphics().getFontRenderContext(); } /** * Draws a string at <code>(x, y)</code>. The start of the text at the * baseline level will be aligned with the <code>(x, y)</code> point. * * @param str the string (<code>null</code> not permitted). * @param x the x-coordinate. * @param y the y-coordinate. * * @see #drawString(java.lang.String, float, float) */ @Override public void drawString(String str, int x, int y) { drawString(str, (float) x, (float) y); } /** * Draws a string at <code>(x, y)</code>. The start of the text at the * baseline level will be aligned with the <code>(x, y)</code> point. * * @param str the string (<code>null</code> not permitted). * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void drawString(String str, float x, float y) { if (str == null) { throw new NullPointerException("Null 'str' argument."); } sb.append("ctx.save();"); if (this.paint instanceof Color) { this.sb.append("ctx.fillStyle=\"").append(toCSSColorValue((Color) this.paint)).append("\";"); } else { setPaint(this.paint); } sb.append("ctx.fillText(\"").append(str).append("\",").append(geomDP(x)).append(",").append(geomDP(y)) .append(");"); sb.append("ctx.restore();"); } /** * Draws a string of attributed characters at <code>(x, y)</code>. The * call is delegated to * {@link #drawString(java.text.AttributedCharacterIterator, float, float)}. * * @param iterator an iterator for the characters. * @param x the x-coordinate. * @param y the x-coordinate. */ @Override public void drawString(AttributedCharacterIterator iterator, int x, int y) { drawString(iterator, (float) x, (float) y); } /** * Draws a string of attributed characters at <code>(x, y)</code>. * * @param iterator an iterator over the characters (<code>null</code> not * permitted). * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void drawString(AttributedCharacterIterator iterator, float x, float y) { TextLayout layout = new TextLayout(iterator, getFontRenderContext()); layout.draw(this, x, y); } /** * Draws the specified glyph vector at the location (x, y). * * @param g the glyph vector. * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void drawGlyphVector(GlyphVector g, float x, float y) { fill(g.getOutline(x, y)); } /** * Translates the origin to <code>(tx, ty)</code>. This call is delegated * to {@link #translate(double, double)}. * * @param tx the x-translation. * @param ty the y-translation. */ @Override public void translate(int tx, int ty) { translate((double) tx, (double) ty); } /** * Applies the translation (tx, ty). * * @param tx the x-translation. * @param ty the y-translation. */ @Override public void translate(double tx, double ty) { AffineTransform t = getTransform(); t.translate(tx, ty); setTransform(t); } /** * Applies a rotation (anti-clockwise) about <code>(0, 0)</code>. * * @param theta the rotation angle (in radians). */ @Override public void rotate(double theta) { AffineTransform t = AffineTransform.getRotateInstance(theta); transform(t); } /** * Applies a rotation (anti-clockwise) about <code>(x, y)</code>. * * @param theta the rotation angle (in radians). * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void rotate(double theta, double x, double y) { translate(x, y); rotate(theta); translate(-x, -y); } /** * Applies a scale transformation. * * @param sx the x-scaling factor. * @param sy the y-scaling factor. */ @Override public void scale(double sx, double sy) { AffineTransform t = getTransform(); t.scale(sx, sy); setTransform(t); } /** * Applies a shear transformation. This is equivalent to the following * call to the <code>transform</code> method: * <br><br> * <ul><li> * <code>transform(AffineTransform.getShearInstance(shx, shy));</code> * </ul> * * @param shx the x-shear factor. * @param shy the y-shear factor. */ @Override public void shear(double shx, double shy) { transform(AffineTransform.getShearInstance(shx, shy)); } /** * Applies this transform to the existing transform by concatenating it. * * @param t the transform (<code>null</code> not permitted). */ @Override public void transform(AffineTransform t) { this.sb.append("ctx.transform("); this.sb.append(transformDP(t.getScaleX())).append(","); // m00 this.sb.append(transformDP(t.getShearY())).append(","); // m10 this.sb.append(transformDP(t.getShearX())).append(","); // m01 this.sb.append(transformDP(t.getScaleY())).append(","); // m11 this.sb.append(transformDP(t.getTranslateX())).append(","); // m02 this.sb.append(transformDP(t.getTranslateY())); // m12 this.sb.append(");"); AffineTransform tx = getTransform(); tx.concatenate(t); setTransform(tx); } /** * Returns a copy of the current transform. * * @return A copy of the current transform (never <code>null</code>). * * @see #setTransform(java.awt.geom.AffineTransform) */ @Override public AffineTransform getTransform() { return (AffineTransform) this.transform.clone(); } /** * Sets the transform. * * @param t the new transform (<code>null</code> permitted, resets to the * identity transform). */ @Override public void setTransform(AffineTransform t) { if (t == null) { this.transform = new AffineTransform(); } else { this.transform = new AffineTransform(t); } this.sb.append("ctx.setTransform("); this.sb.append(transformDP(transform.getScaleX())).append(","); // m00 this.sb.append(transformDP(transform.getShearY())).append(","); // m10 this.sb.append(transformDP(transform.getShearX())).append(","); // m01 this.sb.append(transformDP(transform.getScaleY())).append(","); // m11 this.sb.append(transformDP(transform.getTranslateX())).append(","); // m02 this.sb.append(transformDP(transform.getTranslateY())); // m12 this.sb.append(");"); } /** * Returns <code>true</code> if the rectangle (in device space) intersects * with the shape (the interior, if <code>onStroke</code> is false, * otherwise the stroked outline of the shape). * * @param rect a rectangle (in device space). * @param s the shape. * @param onStroke test the stroked outline only? * * @return A boolean. */ @Override public boolean hit(Rectangle rect, Shape s, boolean onStroke) { Shape ts; if (onStroke) { ts = this.transform.createTransformedShape(this.stroke.createStrokedShape(s)); } else { ts = this.transform.createTransformedShape(s); } if (!rect.getBounds2D().intersects(ts.getBounds2D())) { return false; } Area a1 = new Area(rect); Area a2 = new Area(ts); a1.intersect(a2); return !a1.isEmpty(); } /** * Not yet implemented. */ @Override public void setPaintMode() { throw new UnsupportedOperationException("Not supported yet."); //TODO } /** * Not yet implemented. * * @param c1 the color. */ @Override public void setXORMode(Color c1) { throw new UnsupportedOperationException("Not supported yet."); //TODO } /** * Returns the clip bounds. * * @return The clip bounds (possibly <code>null</code>). */ @Override public Rectangle getClipBounds() { if (this.clip == null) { return null; } return getClip().getBounds(); } /** * Returns the user clipping region. The initial default value is * <code>null</code>. * * @return The user clipping region (possibly <code>null</code>). * * @see #setClip(java.awt.Shape) */ @Override public Shape getClip() { if (this.clip == null) { return null; } AffineTransform inv; try { inv = this.transform.createInverse(); return inv.createTransformedShape(this.clip); } catch (NoninvertibleTransformException ex) { return null; } } /** * Sets the user clipping region. * * @param shape the new user clipping region (<code>null</code> permitted). * * @see #getClip() */ @Override public void setClip(Shape shape) { // null is handled fine here... this.clip = this.transform.createTransformedShape(shape); } /** * Clips to the intersection of the current clipping region and the * specified shape. * * According to the Oracle API specification, this method will accept a * <code>null</code> argument, but there is an open bug report (since 2004) * that suggests this is wrong: * * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189 * * @param s the clip shape (<code>null</code> not permitted). */ @Override public void clip(Shape s) { if (this.clip == null) { setClip(s); return; } Shape ts = this.transform.createTransformedShape(s); if (!ts.intersects(this.clip.getBounds2D())) { setClip(new Rectangle2D.Double()); } else { Area a1 = new Area(ts); Area a2 = new Area(this.clip); a1.intersect(a2); this.clip = new Path2D.Double(a1); } } /** * Clips to the intersection of the current clipping region and the * specified rectangle. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. */ @Override public void clipRect(int x, int y, int width, int height) { setRect(x, y, width, height); clip(this.rect); } /** * Sets the user clipping region to the specified rectangle. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @see #getClip() */ @Override public void setClip(int x, int y, int width, int height) { setRect(x, y, width, height); setClip(this.rect); } /** * Not yet implemented. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param dx the destination x-offset. * @param dy the destination y-offset. */ @Override public void copyArea(int x, int y, int width, int height, int dx, int dy) { throw new UnsupportedOperationException("Not supported yet."); //TODO } /** * Draws a line from <code>(x1, y1)</code> to <code>(x2, y2)</code> using * the current <code>paint</code> and <code>stroke</code>. * * @param x1 the x-coordinate of the start point. * @param y1 the y-coordinate of the start point. * @param x2 the x-coordinate of the end point. * @param y2 the x-coordinate of the end point. */ @Override public void drawLine(int x1, int y1, int x2, int y2) { if (this.line == null) { this.line = new Line2D.Double(x1, y1, x2, y2); } else { this.line.setLine(x1, y1, x2, y2); } draw(this.line); } /** * Fills the specified rectangle with the current <code>paint</code>. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the rectangle width. * @param height the rectangle height. */ @Override public void fillRect(int x, int y, int width, int height) { setRect(x, y, width, height); fill(this.rect); } /** * Clears the specified rectangle by filling it with the current * background color. If the background color is <code>null</code>, this * method will do nothing. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @see #getBackground() */ @Override public void clearRect(int x, int y, int width, int height) { if (getBackground() == null) { return; // we can't do anything } Paint saved = getPaint(); setPaint(getBackground()); fillRect(x, y, width, height); setPaint(saved); } /** * Draws a rectangle with rounded corners using the current * <code>paint</code> and <code>stroke</code>. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param arcWidth the arc-width. * @param arcHeight the arc-height. * * @see #fillRoundRect(int, int, int, int, int, int) */ @Override public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { setRoundRect(x, y, width, height, arcWidth, arcHeight); draw(this.roundRect); } /** * Fills a rectangle with rounded corners using the current * <code>paint</code>. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param arcWidth the arc-width. * @param arcHeight the arc-height. * * @see #drawRoundRect(int, int, int, int, int, int) */ @Override public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { setRoundRect(x, y, width, height, arcWidth, arcHeight); fill(this.roundRect); } /** * Draws an oval framed by the rectangle <code>(x, y, width, height)</code> * using the current <code>paint</code> and <code>stroke</code>. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @see #fillOval(int, int, int, int) */ @Override public void drawOval(int x, int y, int width, int height) { setOval(x, y, width, height); draw(this.oval); } /** * Fills an oval framed by the rectangle <code>(x, y, width, height)</code>. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @see #drawOval(int, int, int, int) */ @Override public void fillOval(int x, int y, int width, int height) { setOval(x, y, width, height); fill(this.oval); } /** * Draws an arc contained within the rectangle * <code>(x, y, width, height)</code>, starting at <code>startAngle</code> * and continuing through <code>arcAngle</code> degrees using * the current <code>paint</code> and <code>stroke</code>. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param startAngle the start angle in degrees, 0 = 3 o'clock. * @param arcAngle the angle (anticlockwise) in degrees. * * @see #fillArc(int, int, int, int, int, int) */ @Override public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { setArc(x, y, width, height, startAngle, arcAngle); draw(this.arc); } /** * Fills an arc contained within the rectangle * <code>(x, y, width, height)</code>, starting at <code>startAngle</code> * and continuing through <code>arcAngle</code> degrees, using * the current <code>paint</code> * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param startAngle the start angle in degrees, 0 = 3 o'clock. * @param arcAngle the angle (anticlockwise) in degrees. * * @see #drawArc(int, int, int, int, int, int) */ @Override public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { setArc(x, y, width, height, startAngle, arcAngle); fill(this.arc); } /** * Draws the specified multi-segment line using the current * <code>paint</code> and <code>stroke</code>. * * @param xPoints the x-points. * @param yPoints the y-points. * @param nPoints the number of points to use for the polyline. */ @Override public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, false); draw(p); } /** * Draws the specified polygon using the current <code>paint</code> and * <code>stroke</code>. * * @param xPoints the x-points. * @param yPoints the y-points. * @param nPoints the number of points to use for the polygon. * * @see #fillPolygon(int[], int[], int) */ @Override public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, true); draw(p); } /** * Fills the specified polygon using the current <code>paint</code>. * * @param xPoints the x-points. * @param yPoints the y-points. * @param nPoints the number of points to use for the polygon. * * @see #drawPolygon(int[], int[], int) */ @Override public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, true); fill(p); } /** * Draws an image with the specified transform. Note that the * <code>observer</code> is ignored. * * @param img the image. * @param xform the transform. * @param obs the image observer (ignored). * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { AffineTransform savedTransform = getTransform(); transform(xform); boolean result = drawImage(img, 0, 0, obs); setTransform(savedTransform); return result; } /** * Draws the image resulting from applying the <code>BufferedImageOp</code> * to the specified image at the location <code>(x, y)</code>. * * @param img the image. * @param op the operation. * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { BufferedImage imageToDraw = op.filter(img, null); drawImage(imageToDraw, new AffineTransform(1f, 0f, 0f, 1f, x, y), null); } /** * Draws the rendered image. * * @param img the image. * @param xform the transform. */ @Override public void drawRenderedImage(RenderedImage img, AffineTransform xform) { BufferedImage bi = GraphicsUtils.convertRenderedImage(img); drawImage(bi, xform, null); } /** * Draws the renderable image. * * @param img the renderable image. * @param xform the transform. */ @Override public void drawRenderableImage(RenderableImage img, AffineTransform xform) { RenderedImage ri = img.createDefaultRendering(); drawRenderedImage(ri, xform); } /** * Draws an image at the location <code>(x, y)</code>. Note that the * <code>observer</code> is ignored. * * @param img the image. * @param x the x-coordinate. * @param y the y-coordinate. * @param observer ignored. * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, int x, int y, ImageObserver observer) { int w = img.getWidth(null); if (w < 0) { return false; } int h = img.getHeight(null); if (h < 0) { return false; } return drawImage(img, x, y, w, h, observer); } /** * Not yet supported (but no exception is thrown). * * @param img the image. * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param observer the observer (<code>null</code> permitted). * * @return A boolean. */ @Override public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { // TODO : implement this return false; } /** * Draws an image at the location <code>(x, y)</code>. Note that the * <code>observer</code> is ignored. * * @param img the image. * @param x the x-coordinate. * @param y the y-coordinate. * @param bgcolor the background color (<code>null</code> permitted). * @param observer ignored. * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { int w = img.getWidth(null); if (w < 0) { return false; } int h = img.getHeight(null); if (h < 0) { return false; } return drawImage(img, x, y, w, h, bgcolor, observer); } /** * Draws an image to the rectangle <code>(x, y, w, h)</code> (scaling it if * required), first filling the background with the specified color. Note * that the <code>observer</code> is ignored. * * @param img the image. * @param x the x-coordinate. * @param y the y-coordinate. * @param w the width. * @param h the height. * @param bgcolor the background color (<code>null</code> permitted). * @param observer ignored. * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, int x, int y, int w, int h, Color bgcolor, ImageObserver observer) { Paint saved = getPaint(); setPaint(bgcolor); fillRect(x, y, w, h); setPaint(saved); return drawImage(img, x, y, w, h, observer); } /** * Draws part of an image (defined by the source rectangle * <code>(sx1, sy1, sx2, sy2)</code>) into the destination rectangle * <code>(dx1, dy1, dx2, dy2)</code>. Note that the <code>observer</code> * is ignored. * * @param img the image. * @param dx1 the x-coordinate for the top left of the destination. * @param dy1 the y-coordinate for the top left of the destination. * @param dx2 the x-coordinate for the bottom right of the destination. * @param dy2 the y-coordinate for the bottom right of the destination. * @param sx1 the x-coordinate for the top left of the source. * @param sy1 the y-coordinate for the top left of the source. * @param sx2 the x-coordinate for the bottom right of the source. * @param sy2 the y-coordinate for the bottom right of the source. * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { int w = dx2 - dx1; int h = dy2 - dy1; BufferedImage img2 = new BufferedImage(BufferedImage.TYPE_INT_ARGB, w, h); Graphics2D g2 = img2.createGraphics(); g2.drawImage(img, 0, 0, w, h, sx1, sy1, sx2, sy2, null); return drawImage(img2, dx1, dx2, null); } /** * Draws part of an image (defined by the source rectangle * <code>(sx1, sy1, sx2, sy2)</code>) into the destination rectangle * <code>(dx1, dy1, dx2, dy2)</code>. The destination rectangle is first * cleared by filling it with the specified <code>bgcolor</code>. Note that * the <code>observer</code> is ignored. * * @param img the image. * @param dx1 the x-coordinate for the top left of the destination. * @param dy1 the y-coordinate for the top left of the destination. * @param dx2 the x-coordinate for the bottom right of the destination. * @param dy2 the y-coordinate for the bottom right of the destination. * @param sx1 the x-coordinate for the top left of the source. * @param sy1 the y-coordinate for the top left of the source. * @param sx2 the x-coordinate for the bottom right of the source. * @param sy2 the y-coordinate for the bottom right of the source. * @param bgcolor the background color (<code>null</code> permitted). * @param observer ignored. * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) { Paint saved = getPaint(); setPaint(bgcolor); fillRect(dx1, dy1, dx2 - dx1, dy2 - dy1); setPaint(saved); return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); } /** * Does nothing. */ @Override public void dispose() { // nothing to do } /** * Returns the script that has been generated by calls to this * <code>Graphics2D</code> implementation. * * @return The script. */ public String getScript() { return this.sb.toString(); } private String transformDP(double d) { if (this.transformFormat != null) { return transformFormat.format(d); } else { return String.valueOf(d); } } private String geomDP(double d) { if (this.geometryFormat != null) { return geometryFormat.format(d); } else { return String.valueOf(d); } } /** * Sets the attributes of the reusable {@link Rectangle2D} object that is * used by the {@link CanvasGraphics2D#drawRect(int, int, int, int)} and * {@link CanvasGraphics2D#fillRect(int, int, int, int)} methods. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. */ private void setRect(int x, int y, int width, int height) { if (this.rect == null) { this.rect = new Rectangle2D.Double(x, y, width, height); } else { this.rect.setRect(x, y, width, height); } } /** * Sets the attributes of the reusable {@link RoundRectangle2D} object that * is used by the {@link #drawRoundRect(int, int, int, int, int, int)} and * {@link #fillRoundRect(int, int, int, int, int, int)} methods. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param arcWidth the arc width. * @param arcHeight the arc height. */ private void setRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { if (this.roundRect == null) { this.roundRect = new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight); } else { this.roundRect.setRoundRect(x, y, width, height, arcWidth, arcHeight); } } /** * Sets the attributes of the reusable {@link Arc2D} object that is used by * {@link #drawArc(int, int, int, int, int, int)} and * {@link #fillArc(int, int, int, int, int, int)} methods. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param startAngle the start angle in degrees, 0 = 3 o'clock. * @param arcAngle the angle (anticlockwise) in degrees. */ private void setArc(int x, int y, int width, int height, int startAngle, int arcAngle) { if (this.arc == null) { this.arc = new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN); } else { this.arc.setArc(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN); } } /** * Sets the attributes of the reusable {@link Ellipse2D} object that is * used by the {@link #drawOval(int, int, int, int)} and * {@link #fillOval(int, int, int, int)} methods. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. */ private void setOval(int x, int y, int width, int height) { if (this.oval == null) { this.oval = new Ellipse2D.Double(x, y, width, height); } else { this.oval.setFrame(x, y, width, height); } } }