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.CubicCurve2D; |
||
| 9 | import java.awt.geom.Point2D; |
||
| 10 | |||
| 11 | /** |
||
| 12 | * <p>Draw a cubic Bézier curve</p> |
||
| 13 | * |
||
| 14 | * <p>Bezier curves are computed using a deceptively simple interpolation technique. A linear Bézier curve is computed by |
||
| 15 | * plotting the points between Point 1 and Point 2 — resulting in a straight line:</p> |
||
| 16 | * |
||
| 17 | * <p><img src="doc-files/Line.png" alt="Linear Bézier curve"></p> |
||
| 18 | * |
||
| 19 | * <p>A quadratic Bézier curve uses a control point in addition to Points 1 and 2. The points on the curve are plotted |
||
| 20 | * by drawing a line from a point on the line from Point 1 to the control point and connecting to a point on the line |
||
| 21 | * connecting the control point to point 2. This is done in a fractional progression: we connect the point, say, 20% of |
||
| 22 | * the way between Point 1 and the control point to a point 20% of the way from the control point to Point 2, and find |
||
| 23 | * the point on the curve 20% of the way along our interpolated line.</p> |
||
| 24 | * |
||
| 25 | * <table> |
||
| 26 | * <tr> |
||
| 27 | * <td><img src="doc-files/Bezier-QuadCurve-fig2.png" alt="Quadratic Bézier curve interpolation"></td> |
||
| 28 | * <td><img src="doc-files/Bezier-QuadCurve-fig3.png" alt="Quadratic Bézier curve interpolated"></td> |
||
| 29 | * <td><img src="doc-files/Bezier-QuadCurve-fig4.png" alt="Quadratic Bézier curve"></td> |
||
| 30 | * </tr> |
||
| 31 | * <caption>Quadratic Bézier curve</caption> |
||
| 32 | * </table> |
||
| 33 | * |
||
| 34 | * <p>We can extend this process to higher dimensions by using an increasing number of intermediate control points |
||
| 35 | * between Points 1 and 2, and increasing the levels of interpolation between those points. A cubic Bézier curve has |
||
| 36 | * two control points.</p> |
||
| 37 | * |
||
| 38 | * <p><img src="doc-files/Bezier-CubicCurve-fig1.png" alt="Cubic Bézier curve with control points"></p> |
||
| 39 | * |
||
| 40 | * <p>We have three imaginary lines: from Point 1 to the first control point, from the first control point to the |
||
| 41 | * second control point, and from the second control point to Point 2. We interpolate lines connecting these lines in |
||
| 42 | * the same manner as a quadratic curve.</p> |
||
| 43 | * |
||
| 44 | * <p><img src="doc-files/Bezier-CubicCurve-fig2.png" alt="First order interpolation of cubic Bézier curve"></p> |
||
| 45 | * |
||
| 46 | * <p>We now interpolate our interpolations and choose points on these second order interpolations as the points of our |
||
| 47 | * curve. To make this somewhat clearer, we start by reducing the number of interpolations for somewhat better clarity.</p> |
||
| 48 | * |
||
| 49 | * <table> |
||
| 50 | * <tr> |
||
| 51 | * <td><img src="doc-files/Bezier-CubicCurve-fig3.png" alt="Second order interpolation of cubic Bézier curve"></td> |
||
| 52 | * <td><img src="doc-files/Bezier-CubicCurve-fig4.png" alt="Second order interpolation of cubic Bézier curve (increased detail)"></td> |
||
| 53 | * <td><img src="doc-files/Bezier-CubicCurve-fig5.png" alt="Cubic Bézier curve"></td> |
||
| 54 | * </tr> |
||
| 55 | * <caption>Cubic Bézier curve</caption> |
||
| 56 | * </table> |
||
| 57 | * |
||
| 58 | * <p>Refer to the <a href="https://en.wikipedia.org/wiki/B%C3%A9zier_curve">Wikipedia Bézier curve article</a> for some |
||
| 59 | * lovely animated GIFs of this process.</p> |
||
| 60 | * |
||
| 61 | * @author <a href="https://github.com/gann-cdf/graphics/issues" target="_blank">Seth Battis</a> |
||
| 62 | */ |
||
| 63 | public class CubicCurve extends Drawable { |
||
| 64 | /** |
||
| 65 | * <p>Construct a new cubic curve.</p> |
||
| 66 | * |
||
| 67 | * <p>A cubic curve is a curve that is shaped like a cubic function (i.e. <i>f(x)</i> = <i>x</i><sup>3</sup>). |
||
| 68 | * The curve is defined by two end points and two control points. The curve is drawn by finding the curve between |
||
| 69 | * the two end points that passes closest to the control points, so adjusting their positions will change the shape of |
||
| 70 | * the curve.</p> |
||
| 71 | * |
||
| 72 | * <p><img src="doc-files/CubicCurve.png" alt="Diagram of CubicCurve parameters"></p> |
||
| 73 | * |
||
| 74 | * <p>All window coordinates are measured in pixels, with the X-axis increasing from left to right and the Y-axis |
||
| 75 | * increasing from top to bottom. All window coordinates exist in the first quadrant.</p> |
||
| 76 | * |
||
| 77 | * <p><img src="../doc-files/window-coordinates.png" alt="Diagram of window coordinates"></p> |
||
| 78 | * |
||
| 79 | * @param x1 X-coordinate of starting point |
||
| 80 | * @param y1 Y-coordinate of staring point |
||
| 81 | * @param ctrlX1 X-coordinate of Control Point 1 |
||
| 82 | * @param ctrlY1 Y-coordinate of Control Point 1 |
||
| 83 | * @param ctrlX2 X-coordinate of Control Point 2 |
||
| 84 | * @param ctrlY2 Y-coordinate of Control Point 2 |
||
| 85 | * @param x2 X-coordinate of end point |
||
| 86 | * @param y2 Y-coordinate of end point |
||
| 87 | * @param drawingPanel on which to draw |
||
| 88 | */ |
||
| 89 | public CubicCurve(double x1, double y1, double ctrlX1, double ctrlY1, double ctrlX2, double ctrlY2, double x2, double y2, DrawingPanel drawingPanel) { |
||
|
0 ignored issues
–
show
Comprehensibility
introduced
by
Loading history...
|
|||
| 90 | try { |
||
| 91 | setShape(new CubicCurve2D.Double(x1, y1, ctrlX1, ctrlY1, ctrlX2, ctrlY2, x2, y2)); |
||
| 92 | setDrawingPanel(drawingPanel); |
||
| 93 | } catch (DrawableException e) { |
||
| 94 | System.err.println(e.getMessage()); |
||
| 95 | e.printStackTrace(); |
||
|
0 ignored issues
–
show
|
|||
| 96 | } |
||
| 97 | } |
||
| 98 | |||
| 99 | /** |
||
| 100 | * @return Underlying {@link CubicCurve2D} geometry |
||
| 101 | */ |
||
| 102 | protected CubicCurve2D getShapeAsCubicCurve() { |
||
| 103 | return (CubicCurve2D) getShape(); |
||
| 104 | } |
||
| 105 | |||
| 106 | @Override |
||
| 107 | public void setShape(Shape shape) throws DrawableException { |
||
| 108 | if (shape instanceof CubicCurve2D) { |
||
| 109 | super.setShape(shape); |
||
| 110 | } else { |
||
| 111 | throw new DrawableException("Attempt to set CubicCurve's underlying shape to a non-CubicCurve2D instance"); |
||
| 112 | } |
||
| 113 | } |
||
| 114 | |||
| 115 | View Code Duplication | @Override |
|
|
0 ignored issues
–
show
|
|||
| 116 | public void setWidth(double width) { |
||
| 117 | // FIXME this feels hacktacular |
||
|
0 ignored issues
–
show
|
|||
| 118 | double scale = width / getWidth(); |
||
| 119 | setCurve( |
||
| 120 | getX() + (getX1() - getX()) * scale, getY1(), |
||
| 121 | getX() + (getCtrlX1() - getX()) * scale, getCtrlY1(), |
||
| 122 | getX() + (getCtrlX2() - getX()) * scale, getCtrlY2(), |
||
| 123 | getX() + (getX2() - getX()) * scale, getY2() |
||
| 124 | ); |
||
| 125 | } |
||
| 126 | |||
| 127 | View Code Duplication | @Override |
|
|
0 ignored issues
–
show
|
|||
| 128 | public void setHeight(double height) { |
||
| 129 | // FIXME this feels hacktackular |
||
|
0 ignored issues
–
show
|
|||
| 130 | double scale = height / getHeight(); |
||
| 131 | setCurve( |
||
| 132 | getX1(), getY() + (getY1() - getY()) * scale, |
||
| 133 | getCtrlX1(), getY() + (getCtrlY1() - getY()) * scale, |
||
| 134 | getCtrlX2(), getY() + (getCtrlY2() - getY()) * scale, |
||
| 135 | getX2(), getY() + (getY2() - getY()) * scale |
||
| 136 | ); |
||
| 137 | } |
||
| 138 | |||
| 139 | /** |
||
| 140 | * Starting point |
||
| 141 | * |
||
| 142 | * @return Coordinates of starting point |
||
| 143 | * @see CubicCurve2D#getP1() |
||
| 144 | */ |
||
| 145 | public Point2D getP1() { |
||
| 146 | return getShapeAsCubicCurve().getP1(); |
||
| 147 | } |
||
| 148 | |||
| 149 | /** |
||
| 150 | * Ending point |
||
| 151 | * |
||
| 152 | * @return Coordinates of ending point |
||
| 153 | * @see CubicCurve2D#getP2() |
||
| 154 | */ |
||
| 155 | public Point2D getP2() { |
||
| 156 | return getShapeAsCubicCurve().getP2(); |
||
| 157 | } |
||
| 158 | |||
| 159 | /** |
||
| 160 | * <p>Set the points describing the curve</p> |
||
| 161 | * |
||
| 162 | * <p>This will leave other characteristics (e.g. fill color or stroke) unchanged</p> |
||
| 163 | * |
||
| 164 | * <p><img src="doc-files/CubicCurve.png" alt="Diagram of setCurve() Parameters"></p> |
||
| 165 | * |
||
| 166 | * @param x1 X-coordinate of starting point |
||
| 167 | * @param y1 Y-coordinate of starting point |
||
| 168 | * @param ctrlX1 X-coordinate of first control point |
||
| 169 | * @param ctrlY1 Y-coordinate of first control point |
||
| 170 | * @param ctrlX2 X-coordinate of second control point |
||
| 171 | * @param ctrlY2 Y-coordinate of second control point |
||
| 172 | * @param x2 X-coordinate of ending point |
||
| 173 | * @param y2 Y-coordinate of ending point |
||
| 174 | */ |
||
| 175 | public void setCurve(double x1, double y1, double ctrlX1, double ctrlY1, double ctrlX2, double ctrlY2, double x2, double y2) { |
||
|
0 ignored issues
–
show
|
|||
| 176 | getShapeAsCubicCurve().setCurve(x1, y1, ctrlX1, ctrlY1, ctrlX2, ctrlY2, x2, y2); |
||
| 177 | } |
||
| 178 | |||
| 179 | /** |
||
| 180 | * X-coordinate of starting point |
||
| 181 | * |
||
| 182 | * @return X-coordinate of starting point |
||
| 183 | * @see CubicCurve2D#getX1() |
||
| 184 | */ |
||
| 185 | public double getX1() { |
||
| 186 | return getShapeAsCubicCurve().getX1(); |
||
| 187 | } |
||
| 188 | |||
| 189 | /** |
||
| 190 | * Y-coordinate of starting point |
||
| 191 | * |
||
| 192 | * @return Y-coordinate of starting point |
||
| 193 | * @see CubicCurve2D#getY1() |
||
| 194 | */ |
||
| 195 | public double getY1() { |
||
| 196 | return getShapeAsCubicCurve().getY1(); |
||
| 197 | } |
||
| 198 | |||
| 199 | /** |
||
| 200 | * X-coordinate of first control point |
||
| 201 | * |
||
| 202 | * @return X-coordinate of first control point |
||
| 203 | * @see CubicCurve2D#getCtrlX1() |
||
| 204 | */ |
||
| 205 | public double getCtrlX1() { |
||
| 206 | return getShapeAsCubicCurve().getCtrlX1(); |
||
| 207 | } |
||
| 208 | |||
| 209 | /** |
||
| 210 | * Y-coordinate of first control point |
||
| 211 | * |
||
| 212 | * @return Y-coordinate of first control point |
||
| 213 | * @see CubicCurve2D#getCtrlY1() |
||
| 214 | */ |
||
| 215 | public double getCtrlY1() { |
||
| 216 | return getShapeAsCubicCurve().getCtrlY1(); |
||
| 217 | } |
||
| 218 | |||
| 219 | /** |
||
| 220 | * First control point |
||
| 221 | * |
||
| 222 | * @return First control point |
||
| 223 | * @see CubicCurve2D#getCtrlP1() |
||
| 224 | */ |
||
| 225 | public Point2D getCtrlP1() { |
||
| 226 | return getShapeAsCubicCurve().getCtrlP1(); |
||
| 227 | } |
||
| 228 | |||
| 229 | /** |
||
| 230 | * X-coordinate of second control point |
||
| 231 | * |
||
| 232 | * @return X-coordinate of first control point |
||
| 233 | * @see CubicCurve2D#getCtrlX2() |
||
| 234 | */ |
||
| 235 | public double getCtrlX2() { |
||
| 236 | return getShapeAsCubicCurve().getCtrlX2(); |
||
| 237 | } |
||
| 238 | |||
| 239 | /** |
||
| 240 | * Y-coordinate of second control point |
||
| 241 | * |
||
| 242 | * @return Y-coordinate of second control point |
||
| 243 | * @see CubicCurve2D#getCtrlY2() |
||
| 244 | */ |
||
| 245 | public double getCtrlY2() { |
||
| 246 | return getShapeAsCubicCurve().getCtrlY2(); |
||
| 247 | } |
||
| 248 | |||
| 249 | /** |
||
| 250 | * Ending point |
||
| 251 | * |
||
| 252 | * @return Coordinates of ending point |
||
| 253 | * @see CubicCurve2D#getP2() |
||
| 254 | */ |
||
| 255 | public Point2D getCtrlP2() { |
||
| 256 | return getShapeAsCubicCurve().getCtrlP2(); |
||
| 257 | } |
||
| 258 | |||
| 259 | /** |
||
| 260 | * X-coordinate of ending point |
||
| 261 | * |
||
| 262 | * @return X-coordinate of ending point |
||
| 263 | * @see CubicCurve2D#getX2() |
||
| 264 | */ |
||
| 265 | public double getX2() { |
||
| 266 | return getShapeAsCubicCurve().getX2(); |
||
| 267 | } |
||
| 268 | |||
| 269 | /** |
||
| 270 | * Y-coordinate of ending point |
||
| 271 | * |
||
| 272 | * @return Y-coordinate of ending point |
||
| 273 | * @see CubicCurve2D#getY2() |
||
| 274 | */ |
||
| 275 | public double getY2() { |
||
| 276 | return getShapeAsCubicCurve().getY2(); |
||
| 277 | } |
||
| 278 | |||
| 279 | @Override |
||
| 280 | public void translate(double dx, double dy) { |
||
| 281 | getShapeAsCubicCurve().setCurve( |
||
| 282 | getX1() + dx, getY1() + dy, |
||
| 283 | getCtrlX1() + dx, getCtrlY1() + dy, |
||
| 284 | getCtrlX2() + dx, getCtrlY2() + dy, |
||
| 285 | getX2() + dx, getY2() + dy |
||
| 286 | ); |
||
| 287 | } |
||
| 288 | |||
| 289 | @Override |
||
| 290 | public void setLocation(double x, double y) { |
||
| 291 | translate(x - getX(), y - getY()); |
||
| 292 | } |
||
| 293 | } |
||
| 294 |