/******************************************************************************* * Copyright 2012 Geoscience Australia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package au.gov.ga.earthsci.common.math.bezier; import java.io.Serializable; import au.gov.ga.earthsci.common.math.vector.Vector; import au.gov.ga.earthsci.common.util.Validate; /** * An implementation of a Cubic Bezier curve. * <p/> * This class provides helper methods for retrieving the value of the curve at a * point along the curve. * <p/> * This implementation is immutable and discretises the curve by sampling at a * specified number of points along the curve. * * @see http://en.wikipedia.org/wiki/B%C3%A9zier_curve * * @param <V> * * @author Michael de Hoog (michael.dehoog@ga.gov.au) * @author James Navin (james.navin@ga.gov.au) */ public class Bezier<V extends Vector<V>> implements Serializable { private static final long serialVersionUID = 20100823L; /** The default number of points to sample along the curve */ private final static int DEFAULT_NUM_SUBDIVISIONS = 1000; /** The number of points to sample along the curve */ private final int numSubdivisions; // The 4 control points needed to define the cubic bezier /** * The beginning value of the bezier. The curve will pass through this * point. */ private final V begin; /** The control point to use when exiting the beginning point. */ private final V out; /** The control point to use when entering the end point. */ private final V in; /** The end value of the bezier. The curve will pass through this point. */ private final V end; /** The sampled points along the curve */ private double[] percents; /** The length of the curve */ private double length; /** * Construct a new Bezier with the provided control points, using the * default number of subdivisions to sample the curve. * * @param begin * The beginning value of the bezier. The curve will pass through * this point. * @param out * The control point to use when exiting the beginning point * @param in * The control point to use when entering the end point * @param end * The end value of the bezier. The curve will pass through this * point. */ public Bezier(V begin, V out, V in, V end) { this(begin, out, in, end, DEFAULT_NUM_SUBDIVISIONS); } /** * Construct a new Bezier with the provided control points, using the * provided number of subdivisions to sample the curve. * * @param begin * The beginning value of the bezier. The curve will pass through * this point. * @param out * The control point to use when exiting the beginning point * @param in * The control point to use when entering the end point * @param end * The end value of the bezier. The curve will pass through this * point. * @param numSubdivisions * The number of subdivisions to use to sample the curve */ public Bezier(V begin, V out, V in, V end, int numSubdivisions) { Validate.notNull(begin, "A begin value is required"); //$NON-NLS-1$ Validate.notNull(out, "An out value is required"); //$NON-NLS-1$ Validate.notNull(in, "A in value is required"); //$NON-NLS-1$ Validate.notNull(end, "A end value is required"); //$NON-NLS-1$ this.begin = begin; this.end = end; this.out = out; this.in = in; this.numSubdivisions = numSubdivisions; subdivide(); } /** * Samples the bezier curve at {@link #numSubdivisions} points and populates * the {@link #percents} array. */ private void subdivide() { percents = new double[numSubdivisions]; length = 0d; V begin = this.begin.clone(); V end; for (int i = 0; i < numSubdivisions; i++) { double t = (i + 1) / (double) numSubdivisions; end = bezierPointAt(t); length += begin.subtractLocal(end).distance(); percents[i] = length; begin = end; } if (length > 0d) { for (int i = 0; i < numSubdivisions; i++) { percents[i] /= length; } } percents[numSubdivisions - 1] = 1d; } /** * @return The length of the curve */ public double getLength() { return length; } /** * Obtain the point on the curve that is <code>percent</code> of the way * along the curve * * @param percent * The percent along the curve to sample. In range * <code>[0,1]</code> * * @return The value at the given percentage along the curve */ public V pointAt(double percent) { if (percent < 0 || percent > 1) { throw new IllegalArgumentException("Percent must be in range [0,1]. Value '" + percent + "' is illegal."); //$NON-NLS-1$ //$NON-NLS-2$ } int i = 0; while (i < percents.length - 1 && percents[i] <= percent) { i++; } double percentStart = i > 0 ? percents[i - 1] : 0d; double percentWindow = percents[i] - percentStart; double p = (percent - percentStart) / percentWindow; percent = (p + i) / numSubdivisions; return bezierPointAt(percent); } /** * Evaluates the bezier curve at the parametric point <code>t</code> * * @return The value on the curve at the parametric point <code>t</code> */ private V bezierPointAt(double t) { double t2 = t * t; V c = out.subtract(begin).multLocal(3d); V b = in.subtract(out).multLocal(3d).subtractLocal(c); V a = end.subtract(begin).subtractLocal(c).subtractLocal(b); a.multLocal(t2 * t); b.multLocal(t2); c.multLocal(t); a.addLocal(b).addLocal(c).addLocal(begin); return a; } @SuppressWarnings("unused") private V deCasteljau(double t) { V v1 = this.out.subtract(this.begin).multLocal(t).addLocal(this.begin); V v2 = this.in.subtract(this.out).multLocal(t).addLocal(this.out); V v3 = this.end.subtract(this.in).multLocal(t).addLocal(this.in); v3 = v3.subtractLocal(v2).multLocal(t).addLocal(v2); v2 = v2.subtractLocal(v1).multLocal(t).addLocal(v1); return v3.subtractLocal(v2).multLocal(t).addLocal(v2); } /** * @return The number or subdivisions used in this bezier */ public int getNumSubdivisions() { return numSubdivisions; } }