1 package org.opentrafficsim.core.geometry; 2 3 import java.awt.geom.Line2D; 4 5 import org.djutils.draw.point.OrientedPoint2d; 6 import org.djutils.draw.point.Point2d; 7 import org.djutils.exceptions.Throw; 8 import org.opentrafficsim.base.geometry.OtsGeometryException; 9 import org.opentrafficsim.base.geometry.OtsLine2d; 10 11 /** 12 * Generation of Bézier curves. <br> 13 * The class implements the cubic(...) method to generate a cubic Bézier curve using the following formula: B(t) = (1 - 14 * t)<sup>3</sup>P<sub>0</sub> + 3t(1 - t)<sup>2</sup> P<sub>1</sub> + 3t<sup>2</sup> (1 - t) P<sub>2</sub> + t<sup>3</sup> 15 * P<sub>3</sub> where P<sub>0</sub> and P<sub>3</sub> are the end points, and P<sub>1</sub> and P<sub>2</sub> the control 16 * points. <br> 17 * For a smooth movement, one of the standard implementations of the cubic(...) function offered is the case where P<sub>1</sub> 18 * is positioned halfway between P<sub>0</sub> and P<sub>3</sub> starting from P<sub>0</sub> in the direction of P<sub>3</sub>, 19 * and P<sub>2</sub> is positioned halfway between P<sub>3</sub> and P<sub>0</sub> starting from P<sub>3</sub> in the direction 20 * of P<sub>0</sub>.<br> 21 * Finally, an n-point generalization of the Bézier curve is implemented with the bezier(...) function. 22 * <p> 23 * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br> 24 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>. 25 * </p> 26 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a> 27 * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a> 28 */ 29 public final class Bezier 30 { 31 /** The default number of points to use to construct a Bézier curve. */ 32 private static final int DEFAULT_NUM_POINTS = 64; 33 34 /** Cached factorial values. */ 35 private static long[] fact = new long[] {1L, 1L, 2L, 6L, 24L, 120L, 720L, 5040L, 40320L, 362880L, 3628800L, 39916800L, 36 479001600L, 6227020800L, 87178291200L, 1307674368000L, 20922789888000L, 355687428096000L, 6402373705728000L, 37 121645100408832000L, 2432902008176640000L}; 38 39 /** Utility class. */ 40 private Bezier() 41 { 42 // do not instantiate 43 } 44 45 /** 46 * Construct a cubic Bézier curve from start to end with two control points. 47 * @param numPoints the number of points for the Bézier curve 48 * @param start the start point of the Bézier curve 49 * @param control1 the first control point 50 * @param control2 the second control point 51 * @param end the end point of the Bézier curve 52 * @return a cubic Bézier curve between start and end, with the two provided control points 53 * @throws OtsGeometryException in case the number of points is less than 2 or the Bézier curve could not be 54 * constructed 55 */ 56 public static OtsLine2d cubic(final int numPoints, final Point2d start, final Point2d control1, final Point2d control2, 57 final Point2d end) throws OtsGeometryException 58 { 59 Throw.when(numPoints < 2, OtsGeometryException.class, "Number of points too small (got %d; minimum value is 2)", 60 numPoints); 61 Point2d[] points = new Point2d[numPoints]; 62 for (int n = 0; n < numPoints; n++) 63 { 64 double t = n / (numPoints - 1.0); 65 double x = B3(t, start.x, control1.x, control2.x, end.x); 66 double y = B3(t, start.y, control1.y, control2.y, end.y); 67 points[n] = new Point2d(x, y); 68 } 69 return new OtsLine2d(points); 70 } 71 72 /** 73 * Construct a cubic Bézier curve from start to end with two generated control points at half the distance between 74 * start and end. The z-value is interpolated in a linear way. 75 * @param numPoints the number of points for the Bézier curve 76 * @param start the directed start point of the Bézier curve 77 * @param end the directed end point of the Bézier curve 78 * @return a cubic Bézier curve between start and end, with the two provided control points 79 * @throws OtsGeometryException in case the number of points is less than 2 or the Bézier curve could not be 80 * constructed 81 */ 82 public static OtsLine2d cubic(final int numPoints, final OrientedPoint2d start, final OrientedPoint2d end) 83 throws OtsGeometryException 84 { 85 return cubic(numPoints, start, end, 1.0); 86 } 87 88 /** 89 * Construct a cubic Bézier curve from start to end with two generated control points at half the distance between 90 * start and end. The z-value is interpolated in a linear way. 91 * @param numPoints the number of points for the Bézier curve 92 * @param start the directed start point of the Bézier curve 93 * @param end the directed end point of the Bézier curve 94 * @param shape 1 = control points at half the distance between start and end, > 1 results in a pointier shape, < 1 95 * results in a flatter shape, value should be above 0 96 * @return a cubic Bézier curve between start and end, with the two determined control points 97 * @throws OtsGeometryException in case the number of points is less than 2 or the Bézier curve could not be 98 * constructed 99 */ 100 public static OtsLine2d cubic(final int numPoints, final OrientedPoint2d start, final OrientedPoint2d end, 101 final double shape) throws OtsGeometryException 102 { 103 return cubic(numPoints, start, end, shape, false); 104 } 105 106 /** 107 * Construct a cubic Bézier curve from start to end with two generated control points at half the distance between 108 * start and end. The z-value is interpolated in a linear way. 109 * @param numPoints the number of points for the Bézier curve 110 * @param start the directed start point of the Bézier curve 111 * @param end the directed end point of the Bézier curve 112 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier 113 * shape, < 1 results in a flatter shape, value should be above 0 114 * @param weighted control point distance relates to distance to projected point on extended line from other end 115 * @return a cubic Bézier curve between start and end, with the two determined control points 116 * @throws OtsGeometryException in case the number of points is less than 2 or the Bézier curve could not be 117 * constructed 118 */ 119 public static OtsLine2d cubic(final int numPoints, final OrientedPoint2d start, final OrientedPoint2d end, 120 final double shape, final boolean weighted) throws OtsGeometryException 121 { 122 return bezier(cubicControlPoints(start, end, shape, weighted)); 123 } 124 125 /** 126 * Construct control points for a cubic Bézier curve from start to end with two generated control points at half the 127 * distance between start and end. 128 * @param start the directed start point of the Bézier curve 129 * @param end the directed end point of the Bézier curve 130 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier 131 * shape, < 1 results in a flatter shape, value should be above 0 132 * @param weighted control point distance relates to distance to projected point on extended line from other end 133 * @return a cubic Bézier curve between start and end, with the two determined control points 134 */ 135 public static Point2d[] cubicControlPoints(final OrientedPoint2d start, final OrientedPoint2d end, final double shape, 136 final boolean weighted) 137 { 138 Throw.when(shape <= 0.0, IllegalArgumentException.class, "Shape factor must be above 0.0."); 139 Point2d control1; 140 Point2d control2; 141 142 if (weighted) 143 { 144 // each control point is 'w' * the distance between the end-points away from the respective end point 145 // 'w' is a weight given by the distance from the end point to the extended line of the other end point 146 double dx = end.x - start.x; 147 double dy = end.y - start.y; 148 double distance = shape * Math.hypot(dx, dy); 149 double cosEnd = Math.cos(end.getDirZ()); 150 double sinEnd = Math.sin(end.getDirZ()); 151 double dStart = Line2D.ptLineDist(end.x, end.y, end.x + cosEnd, end.y + sinEnd, start.x, start.y); 152 double cosStart = Math.cos(start.getDirZ()); 153 double sinStart = Math.sin(start.getDirZ()); 154 double dEnd = Line2D.ptLineDist(start.x, start.y, start.x + cosStart, start.y + sinStart, end.x, end.y); 155 double wStart = dStart / (dStart + dEnd); 156 double wEnd = dEnd / (dStart + dEnd); 157 double wStartDistance = wStart * distance; 158 double wEndDistance = wEnd * distance; 159 control1 = new Point2d(start.x + wStartDistance * cosStart, start.y + wStartDistance * sinStart); 160 // - (minus) as the angle is where the line leaves, i.e. from shape point to end 161 control2 = new Point2d(end.x - wEndDistance * cosEnd, end.y - wEndDistance * sinEnd); 162 } 163 else 164 { 165 // each control point is half the distance between the end-points away from the respective end point 166 double dx = end.x - start.x; 167 double dy = end.y - start.y; 168 double distance2 = shape * .5 * Math.hypot(dx, dy); 169 control1 = new Point2d(start.x + distance2 * Math.cos(start.getDirZ()), 170 start.y + distance2 * Math.sin(start.getDirZ())); 171 control2 = new Point2d(end.x - distance2 * Math.cos(end.getDirZ()), end.y - distance2 * Math.sin(end.getDirZ())); 172 } 173 174 return new Point2d[] {start, control1, control2, end}; 175 } 176 177 /** 178 * Construct a cubic Bézier curve from start to end with two generated control points at half the distance between 179 * start and end. The z-value is interpolated in a linear way. 180 * @param start the directed start point of the Bézier curve 181 * @param end the directed end point of the Bézier curve 182 * @return a cubic Bézier curve between start and end, with the two provided control points 183 * @throws OtsGeometryException in case the number of points is less than 2 or the Bézier curve could not be 184 * constructed 185 */ 186 public static OtsLine2d cubic(final OrientedPoint2d start, final OrientedPoint2d end) throws OtsGeometryException 187 { 188 return cubic(DEFAULT_NUM_POINTS, start, end); 189 } 190 191 /** 192 * Calculate the cubic Bézier point with B(t) = (1 - t)<sup>3</sup>P<sub>0</sub> + 3t(1 - t)<sup>2</sup> 193 * P<sub>1</sub> + 3t<sup>2</sup> (1 - t) P<sub>2</sub> + t<sup>3</sup> P<sub>3</sub>. 194 * @param t the fraction 195 * @param p0 the first point of the curve 196 * @param p1 the first control point 197 * @param p2 the second control point 198 * @param p3 the end point of the curve 199 * @return the cubic bezier value B(t) 200 */ 201 @SuppressWarnings("checkstyle:methodname") 202 private static double B3(final double t, final double p0, final double p1, final double p2, final double p3) 203 { 204 double t2 = t * t; 205 double t3 = t2 * t; 206 double m = (1.0 - t); 207 double m2 = m * m; 208 double m3 = m2 * m; 209 return m3 * p0 + 3.0 * t * m2 * p1 + 3.0 * t2 * m * p2 + t3 * p3; 210 } 211 212 /** 213 * Construct a Bézier curve of degree n. 214 * @param numPoints the number of points for the Bézier curve to be constructed 215 * @param points the points of the curve, where the first and last are begin and end point, and the intermediate ones are 216 * control points. There should be at least two points. 217 * @return the Bézier value B(t) of degree n, where n is the number of points in the array 218 * @throws OtsGeometryException in case the number of points is less than 2 or the Bézier curve could not be 219 * constructed 220 */ 221 public static OtsLine2d bezier(final int numPoints, final Point2d... points) throws OtsGeometryException 222 { 223 Point2d[] result = new Point2d[numPoints]; 224 double[] px = new double[points.length]; 225 double[] py = new double[points.length]; 226 for (int i = 0; i < points.length; i++) 227 { 228 px[i] = points[i].x; 229 py[i] = points[i].y; 230 } 231 for (int n = 0; n < numPoints; n++) 232 { 233 double t = n / (numPoints - 1.0); 234 double x = Bn(t, px); 235 double y = Bn(t, py); 236 result[n] = new Point2d(x, y); 237 } 238 return new OtsLine2d(result); 239 } 240 241 /** 242 * Construct a Bézier curve of degree n. 243 * @param points the points of the curve, where the first and last are begin and end point, and the intermediate ones are 244 * control points. There should be at least two points. 245 * @return the Bézier value B(t) of degree n, where n is the number of points in the array 246 * @throws OtsGeometryException in case the number of points is less than 2 or the Bézier curve could not be 247 * constructed 248 */ 249 public static OtsLine2d bezier(final Point2d... points) throws OtsGeometryException 250 { 251 return bezier(DEFAULT_NUM_POINTS, points); 252 } 253 254 /** 255 * Calculate the Bézier point of degree n, with B(t) = Sum(i = 0..n) [C(n, i) * (1 - t)<sup>n-i</sup> t<sup>i</sup> 256 * P<sub>i</sub>], where C(n, k) is the binomial coefficient defined by n! / ( k! (n-k)! ), ! being the factorial operator. 257 * @param t the fraction 258 * @param p the points of the curve, where the first and last are begin and end point, and the intermediate ones are control 259 * points 260 * @return the Bézier value B(t) of degree n, where n is the number of points in the array 261 */ 262 @SuppressWarnings("checkstyle:methodname") 263 static double Bn(final double t, final double... p) 264 { 265 double b = 0.0; 266 double m = (1.0 - t); 267 int n = p.length - 1; 268 double fn = factorial(n); 269 for (int i = 0; i <= n; i++) 270 { 271 double c = fn / (factorial(i) * (factorial(n - i))); 272 b += c * Math.pow(m, n - i) * Math.pow(t, i) * p[i]; 273 } 274 return b; 275 } 276 277 /** 278 * Calculate factorial(k), which is k * (k-1) * (k-2) * ... * 1. For factorials up to 20, a lookup table is used. 279 * @param k the parameter 280 * @return factorial(k) 281 */ 282 private static double factorial(final int k) 283 { 284 if (k < fact.length) 285 { 286 return fact[k]; 287 } 288 double f = 1; 289 for (int i = 2; i <= k; i++) 290 { 291 f = f * i; 292 } 293 return f; 294 } 295 296 }