gann-cdf /
graphics
| 1 | package org.gannacademy.cdf.graphics.geom; |
||
| 2 | |||
| 3 | import org.gannacademy.cdf.graphics.Drawable; |
||
| 4 | import org.gannacademy.cdf.graphics.DrawableException; |
||
| 5 | import org.gannacademy.cdf.graphics.ui.DrawingPanel; |
||
| 6 | |||
| 7 | import java.awt.*; |
||
| 8 | import java.awt.geom.AffineTransform; |
||
| 9 | import java.awt.geom.Path2D; |
||
| 10 | |||
| 11 | /** |
||
| 12 | * <p>Draw an arbitrary path</p> |
||
| 13 | * |
||
| 14 | * <p><img src="doc-files/Path.png" alt="Path diagram"></p> |
||
| 15 | * |
||
| 16 | * <p>Paths are constructed from an arbitrary number of segments. Each segment is a {@link Line}, {@link QuadCurve}, or |
||
| 17 | * {@link CubicCurve} drawn between the end point of the prior segment and new point. For example, in the diagram above, |
||
| 18 | * the path is made of three segments:</p> |
||
| 19 | * |
||
| 20 | * <ol> |
||
| 21 | * <li>A line from Point 1 to Point 2</li> |
||
| 22 | * <li>A cubic curve from Point 2 to Point 3</li> |
||
| 23 | * <li>A quadratic curve from Point 3 to Point 4</li> |
||
| 24 | * </ol> |
||
| 25 | * |
||
| 26 | * <p>The path above could be drawn with the following sequence of instructions:</p> |
||
| 27 | * |
||
| 28 | * <pre> |
||
| 29 | * Path p = new Path(); |
||
| 30 | * p.moveTo(20, 16); // Point 1 |
||
| 31 | * p.lineTo(40, 140); // Point 2 |
||
| 32 | * p.curveTo( |
||
| 33 | * 100, 140, // first Bézier control point |
||
| 34 | * 120, 127, // second Bézier control point |
||
| 35 | * 120, 78 // Point 3 |
||
| 36 | * ); |
||
| 37 | * p.quadTo( |
||
| 38 | * 120, 16; // quadratic control point |
||
| 39 | * 220, 78 // Point 4 |
||
| 40 | * ); |
||
| 41 | * </pre> |
||
| 42 | * |
||
| 43 | * <p>All paths start with empty geometry and are then built segment by segment using {@link #moveTo(double, double)}, |
||
| 44 | * {@link #lineTo(double, double)}, {@link #quadTo(double, double, double, double)}, and |
||
| 45 | * {@link #curveTo(double, double, double, double, double, double)} methods to extend the existing geometry.</p> |
||
| 46 | * |
||
| 47 | * <p>Note that the first segment added to the path must be a {@link #moveTo(double, double)} instruction, to locate |
||
| 48 | * the first point in the path. Additional {@link #moveTo(double, double)} calls may be made as the path is defined, |
||
| 49 | * creating a discontinuous path.</p> |
||
| 50 | * |
||
| 51 | * <p>Paths are particularly complex (and therefore flexible and powerful!). As the underlying geometry of this object |
||
| 52 | * is stored as a {@link Path2D}, it is worth perusing that documentation for information on details like approaches to |
||
| 53 | * filling, stroking, or transforming paths. More detailed explanations of how the Bézier curve segments are computed |
||
| 54 | * can be found in {@link QuadCurve} and {@link CubicCurve}.</p> |
||
| 55 | * |
||
| 56 | * @author <a href="https://github.com/gann-cdf/graphics/issues" target="_blank">Seth Battis</a> |
||
| 57 | */ |
||
| 58 | public class Path extends Drawable { |
||
| 59 | /** |
||
| 60 | * <p>Construct a path with empty geometry</p> |
||
| 61 | * |
||
| 62 | * @param drawingPanel on which to draw |
||
| 63 | */ |
||
| 64 | public Path(DrawingPanel drawingPanel) { |
||
| 65 | try { |
||
| 66 | setShape(new Path2D.Double()); |
||
| 67 | setDrawingPanel(drawingPanel); |
||
| 68 | } catch (DrawableException e) { |
||
| 69 | System.err.println(e.getMessage()); |
||
| 70 | e.printStackTrace(); |
||
|
0 ignored issues
–
show
Best Practice
introduced
by
Loading history...
|
|||
| 71 | } |
||
| 72 | } |
||
| 73 | |||
| 74 | /** |
||
| 75 | * <p>Construct a path with empty geometry and a winding rule</p> |
||
| 76 | * |
||
| 77 | * @param windingRule The <a href="https://en.wikipedia.org/wiki/Nonzero-rule#/media/File:Even-odd_and_non-zero_winding_fill_rules.png"> |
||
| 78 | * winding rule</a> to determine how to fill the shape |
||
| 79 | * @param drawingPanel on which to draw |
||
| 80 | */ |
||
| 81 | public Path(int windingRule, DrawingPanel drawingPanel) { |
||
| 82 | try { |
||
| 83 | setShape(new Path2D.Double(windingRule)); |
||
| 84 | setDrawingPanel(drawingPanel); |
||
| 85 | } catch (DrawableException e) { |
||
| 86 | e.printStackTrace(); |
||
|
0 ignored issues
–
show
|
|||
| 87 | } |
||
| 88 | } |
||
| 89 | |||
| 90 | /** |
||
| 91 | * <p>Construct a path with empty geometry, a winding rule and expected number of segments</p> |
||
| 92 | * |
||
| 93 | * <p>The path will expand to contain as many segments as are added to it, but setting the initial capacity to your best |
||
| 94 | * guess gains some small amount of efficiency in reducing resizing operations.</p> |
||
| 95 | * |
||
| 96 | * @param windingRule The <a href="https://en.wikipedia.org/wiki/Nonzero-rule#/media/File:Even-odd_and_non-zero_winding_fill_rules.png"> |
||
| 97 | * winding rule</a> to determine how to fill the shape |
||
| 98 | * @param initialCapacity Anticipated number of segments |
||
| 99 | * @param drawingPanel on which to draw |
||
| 100 | */ |
||
| 101 | public Path(int windingRule, int initialCapacity, DrawingPanel drawingPanel) { |
||
| 102 | try { |
||
| 103 | setShape(new Path2D.Double(windingRule, initialCapacity)); |
||
| 104 | setDrawingPanel(drawingPanel); |
||
| 105 | } catch (DrawableException e) { |
||
| 106 | e.printStackTrace(); |
||
|
0 ignored issues
–
show
|
|||
| 107 | } |
||
| 108 | } |
||
| 109 | |||
| 110 | /** |
||
| 111 | * <p>Construct a path from {@link Shape} geometry and a transformation</p> |
||
| 112 | * |
||
| 113 | * @param shape of underlying geometry |
||
| 114 | * @param transformation to apply {@code shape} (i.e. scale, translation, rotation) |
||
| 115 | * @param drawingPanel on which to draw |
||
| 116 | * @throws DrawableException If {@code shape} cannot be converted to a {@link Path2D} (a highly unlikely eventuality) |
||
| 117 | */ |
||
| 118 | public Path(Shape shape, AffineTransform transformation, DrawingPanel drawingPanel) throws DrawableException { |
||
| 119 | setShape(new Path2D.Double(shape, transformation)); |
||
| 120 | setDrawingPanel(drawingPanel); |
||
| 121 | } |
||
| 122 | |||
| 123 | /** |
||
| 124 | * Underlying {@link Path2D} geometry |
||
| 125 | * |
||
| 126 | * @return Underlying {@link Path2D} geometry |
||
| 127 | */ |
||
| 128 | protected Path2D getShapeAsPath() { |
||
| 129 | return (Path2D) getShape(); |
||
| 130 | } |
||
| 131 | |||
| 132 | @Override |
||
| 133 | public void setShape(Shape shape) throws DrawableException { |
||
| 134 | if (shape instanceof Path2D) { |
||
| 135 | super.setShape(shape); |
||
| 136 | } else { |
||
| 137 | throw new DrawableException("Attempt to set Path's underlying shape to a non-Path2D instance"); |
||
| 138 | } |
||
| 139 | } |
||
| 140 | |||
| 141 | @Override |
||
| 142 | public void setWidth(double width) { |
||
| 143 | // translate to origin before scaling so that distance from origin is not _also_ scaled! |
||
| 144 | double x = getX(); |
||
| 145 | transform(AffineTransform.getTranslateInstance(-x, 0)); |
||
| 146 | transform(AffineTransform.getScaleInstance(width / getWidth(), 1)); |
||
| 147 | transform(AffineTransform.getTranslateInstance(x, 0)); |
||
| 148 | } |
||
| 149 | |||
| 150 | @Override |
||
| 151 | public void setHeight(double height) { |
||
| 152 | // translate to origin before scaling so that distance from origin is not _also_ scaled! |
||
| 153 | double y = getY(); |
||
| 154 | transform(AffineTransform.getTranslateInstance(0, -y)); |
||
| 155 | transform(AffineTransform.getScaleInstance(1, height / getHeight())); |
||
| 156 | transform(AffineTransform.getTranslateInstance(0, y)); |
||
| 157 | } |
||
| 158 | |||
| 159 | /** |
||
| 160 | * <p>Add a cubic curve segment to the path</p> |
||
| 161 | * |
||
| 162 | * <p><img src="doc-files/CubicCurve.png" alt="Cubic Curve diagram"></p> |
||
| 163 | * |
||
| 164 | * <p>The cubic Bézier curve starts at the current end point of the path and extends through two control points. For |
||
| 165 | * more details on how cubic Bézier curves are computes, refer to {@link CubicCurve}.</p> |
||
| 166 | * |
||
| 167 | * @param ctrlX1 X-coordinate of first control point |
||
| 168 | * @param ctrlY1 Y-coordinate of first control point |
||
| 169 | * @param ctrlX2 X-coordinate of second control point |
||
| 170 | * @param ctrlY2 Y-coordinate of second control point |
||
| 171 | * @param x3 X-coordinate of end point |
||
| 172 | * @param y3 Y-coordinate of end point |
||
| 173 | */ |
||
| 174 | public void curveTo(double ctrlX1, double ctrlY1, double ctrlX2, double ctrlY2, double x3, double y3) { |
||
| 175 | getShapeAsPath().curveTo(ctrlX1, ctrlY1, ctrlX2, ctrlY2, x3, y3); |
||
| 176 | } |
||
| 177 | |||
| 178 | /** |
||
| 179 | * <p>Add a line segment to the path</p> |
||
| 180 | * |
||
| 181 | * <p><img src="doc-files/Line.png" alt="Line diagram"></p> |
||
| 182 | * |
||
| 183 | * <p>The line starts at the current end point of the path. For more details on drawing lines, refer to {@link Line}.</p> |
||
| 184 | * |
||
| 185 | * @param x coordinate of end point |
||
| 186 | * @param y coordinate of end point |
||
| 187 | */ |
||
| 188 | public void lineTo(double x, double y) { |
||
| 189 | getShapeAsPath().lineTo(x, y); |
||
| 190 | } |
||
| 191 | |||
| 192 | @Override |
||
| 193 | public void setLocation(double x, double y) { |
||
| 194 | translate(x - getX(), y - getY()); |
||
| 195 | } |
||
| 196 | |||
| 197 | /** |
||
| 198 | * <p>Select a new starting point for subsequent path segments</p> |
||
| 199 | * |
||
| 200 | * <p>Path segments are defined relative to the end point of the previous segment. {@code moveTo()} |
||
| 201 | * must be the first instruction to the path to set a starting point for following segments. This method can also |
||
| 202 | * be used to define a discontinuous path.</p> |
||
| 203 | * |
||
| 204 | * @param x coordinate |
||
| 205 | * @param y coordinate |
||
| 206 | */ |
||
| 207 | public void moveTo(double x, double y) { |
||
| 208 | getShapeAsPath().moveTo(x, y); |
||
| 209 | } |
||
| 210 | |||
| 211 | |||
| 212 | /** |
||
| 213 | * <p>Add a quadratic Bézier curve segment to the path</p> |
||
| 214 | * |
||
| 215 | * <p><img src="doc-files/QuadCurve.png" alt="Quad Curve diagram"></p> |
||
| 216 | * |
||
| 217 | * <p>The quadratic Bézier curve segments starts at the end point of the previous path segment, through a control |
||
| 218 | * point to the end point. For more information on computing quadratic Bézier curves, refer to {@link QuadCurve}.</p> |
||
| 219 | * |
||
| 220 | * @param ctrlX1 X-coordinate of control point |
||
| 221 | * @param ctrlY1 Y-coordinate of control point |
||
| 222 | * @param x2 X-coordinate of end point |
||
| 223 | * @param y2 Y-coordinate of end point |
||
| 224 | */ |
||
| 225 | public void quadTo(double ctrlX1, double ctrlY1, double x2, double y2) { |
||
| 226 | getShapeAsPath().quadTo(ctrlX1, ctrlY1, x2, y2); |
||
| 227 | } |
||
| 228 | |||
| 229 | /** |
||
| 230 | * <p>Close the path by drawing a straight line back to the starting point</p> |
||
| 231 | */ |
||
| 232 | public void closePath() { |
||
| 233 | getShapeAsPath().closePath(); |
||
| 234 | } |
||
| 235 | |||
| 236 | /** |
||
| 237 | * <p>Transform the path</p> |
||
| 238 | * |
||
| 239 | * <p>An "affine transformation" is one in which the spatial relationships of the points of the path are not changed |
||
| 240 | * relative to each other — scale, translation, and rotation. Refer to {@link AffineTransform} for more |
||
| 241 | * information.</p> |
||
| 242 | * |
||
| 243 | * @param transformation to be applied to the path |
||
| 244 | */ |
||
| 245 | public void transform(AffineTransform transformation) { |
||
| 246 | getShapeAsPath().transform(transformation); |
||
| 247 | } |
||
| 248 | |||
| 249 | /** |
||
| 250 | * <p>Translate the shape from one location to another</p> |
||
| 251 | * |
||
| 252 | * <p><img src="doc-files/Path-translate.png" alt="Translation diagram"></p> |
||
| 253 | * |
||
| 254 | * <p>Note that {@code dx} and {@code dy} are the change in in X- and Y- coordinates, and are therefore relative to the current position of the shape, and not an absolute location. (To move a shape to an absolute location, use the {@link #setLocation(double x, double y)} method.)</p> |
||
| 255 | * |
||
| 256 | * @param dx Change in X-coordinates |
||
| 257 | * @param dy Change in Y-coordinates |
||
| 258 | */ |
||
| 259 | @Override |
||
| 260 | public void translate(double dx, double dy) { |
||
| 261 | transform(AffineTransform.getTranslateInstance(dx, dy)); |
||
| 262 | } |
||
| 263 | |||
| 264 | /** |
||
| 265 | * <p>Rotate the shape around an anchor point</p> |
||
| 266 | * |
||
| 267 | * <p><img src="doc-files/Path-rotate.png" alt="Rotation diagram"></p> |
||
| 268 | * |
||
| 269 | * @param theta Angle (in radians) to rotate the shape |
||
| 270 | * @param anchorX X-coordinate of anchor point |
||
| 271 | * @param anchorY Y-coordinate of anchor point |
||
| 272 | */ |
||
| 273 | public void rotate(double theta, double anchorX, double anchorY) { |
||
| 274 | transform(AffineTransform.getRotateInstance(theta, anchorX, anchorY)); |
||
| 275 | } |
||
| 276 | |||
| 277 | /** |
||
| 278 | * <p>Rescale the shape by a factor along the X and Y axes</p> |
||
| 279 | * |
||
| 280 | * <p><img src="doc-files/Path-scale.png" alt="Scaling diagram"></p> |
||
| 281 | * |
||
| 282 | * <p>Note that scaling a shape scales not only its width and height dimensions, but also its position relative to |
||
| 283 | * the origin: that is, its X and Y-coordinates are also scaled. This means that a shape whose top-left bounding |
||
| 284 | * box X and Y-coordinates are not at the origin will have its location changed by the scaling transformation.</p> |
||
| 285 | * |
||
| 286 | * @param scaleFactorX Factor by which to scale the shape in the X direction (as a percentage of the width) |
||
| 287 | * @param scaleFactorY Factor by which to scale the shape in the Y direction (as a percentage of the height); |
||
| 288 | */ |
||
| 289 | public void scale(double scaleFactorX, double scaleFactorY) { |
||
| 290 | transform(AffineTransform.getScaleInstance(scaleFactorX, scaleFactorY)); |
||
| 291 | } |
||
| 292 | |||
| 293 | /** |
||
| 294 | * <p>Shear the shape by a factor along the X and Y axes</p> |
||
| 295 | * |
||
| 296 | * <p><img src="doc-files/Path-shear.png" alt="Shearing diagram"></p> |
||
| 297 | * |
||
| 298 | * <p>Note that shearing a shape shears not only the relative positions of the shape vertices to each other, but |
||
| 299 | * also the position of those vertices relative to the origin. This means that a shape whose top-left bounding box |
||
| 300 | * X and Y-coordinates are not at the origin will have its location changed by the shearing transformation.</p> |
||
| 301 | * |
||
| 302 | * @param shearFactorX Factor by which to shear the shape in the X direction (as a percentage of the width) |
||
| 303 | * @param shearFactorY Factor by which to shear the shape in the Y direction (as a percentage of the height) |
||
| 304 | */ |
||
| 305 | public void shear(double shearFactorX, double shearFactorY) { |
||
| 306 | transform(AffineTransform.getShearInstance(shearFactorX, shearFactorY)); |
||
| 307 | } |
||
| 308 | } |
||
| 309 |