diff --git a/src/main/java/com/epaga/particles/influencers/SizeInfluencer.java b/src/main/java/com/epaga/particles/influencers/SizeInfluencer.java index 4f71ee4..ebff8b5 100644 --- a/src/main/java/com/epaga/particles/influencers/SizeInfluencer.java +++ b/src/main/java/com/epaga/particles/influencers/SizeInfluencer.java @@ -44,6 +44,13 @@ * Size Module * The size module controls the particle size over time * + * To create a size inflencer that linearly changes the particle size from 0.3 to 0.1 over its lifetime create like: + *
{@code
+ * ValueType sizeOverTime = new ValueType(Curve.builder().anchorPoint(0f, 0.03f).anchorPoint(1f, 0.01f).end());
+ * SizeInfluencer sizeInfluencer = new SizeInfluencer();
+ * sizeInfluencer.setSizeOverTime(sizeOverTime);
+ * }
+ *
* @author t0neg0d
* @author Jeddic
*/
diff --git a/src/main/java/com/epaga/particles/valuetypes/Curve.java b/src/main/java/com/epaga/particles/valuetypes/Curve.java
index 831c6a6..cec34bd 100644
--- a/src/main/java/com/epaga/particles/valuetypes/Curve.java
+++ b/src/main/java/com/epaga/particles/valuetypes/Curve.java
@@ -31,6 +31,8 @@
*/
package com.epaga.particles.valuetypes;
+import com.epaga.particles.valuetypes.curvebuilder.CurveBuilderAtAnchor;
+import com.epaga.particles.valuetypes.curvebuilder.CurveBuilderStart;
import com.jme3.export.*;
import com.jme3.math.Vector2f;
@@ -158,4 +160,38 @@ public boolean equals(Object o) {
return true;
}
+
+
+ /**
+ * Produces a builder that can be used to fluently build a curve. A Curve will always be continuous (And should
+ * move in a positive X direction) but the gradient may change sharply.
+ *
+ * It is a series of anchor points connected either by straight line sections or cubic Bézier-like curves (defined by
+ * 2 control points). They are bezier-like curves not Bézier curves because of the requirement that X (often
+ * representing time) can only be allowed to move forward
+ *
+ * In normal usage the first anchor point should be at x = 0, all further points should advance in the X axis and
+ * the final anchor point should have x at 1. This is because usually X is the fractional life of the particle
+ *
+ * Example usage:
+ *
+ * {@code
+ * Curve curve = Curve.builder()
+ * .anchorPoint(new Vector2f(0,0))
+ * .anchorPoint(new Vector2f(0.5f,0.5f))
+ * .controlPoint1(new Vector2f(0.6f,0.5f))
+ * .controlPoint2(new Vector2f(0.8f,2f))
+ * .anchorPoint(new Vector2f(1,2f))
+ * .build();
+ * }
+ *
+ * This example produces a straight line from (0,0) to (0.5,0.5), then a cubic Besier curves between (0.5,0.5) to (1,2) with control points (0.6,0.5) and (0.8,2)
+ *
+ * Note that a builder should not be reused.
+ *
+ * @return a CurveBuilderStart
+ */
+ public static CurveBuilderStart builder(){
+ return new CurveBuilderStart();
+ }
}
diff --git a/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtAnchor.java b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtAnchor.java
new file mode 100644
index 0000000..f53053e
--- /dev/null
+++ b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtAnchor.java
@@ -0,0 +1,71 @@
+package com.epaga.particles.valuetypes.curvebuilder;
+
+import com.epaga.particles.valuetypes.Curve;
+import com.jme3.math.Vector2f;
+
+public class CurveBuilderAtAnchor extends CurveBuilderPiece{
+
+ private final Curve curveBeingBuilt;
+ private final Vector2f controlPointIn;
+ private final Vector2f currentAnchor;
+
+ public CurveBuilderAtAnchor(Curve curveBeingBuilt, Vector2f controlPointIn, Vector2f currentAnchor){
+ this.curveBeingBuilt = curveBeingBuilt;
+ this.controlPointIn = controlPointIn;
+ this.currentAnchor = currentAnchor;
+ }
+
+ /**
+ * Adds a point that the curve will attempt to move towards (but may not actually touch).
+ *
+ * The 2 control points are used to define a cubic Bézier-like curve between 2 anchors
+ * @param x the next control point's x
+ * @param y the next control point's y
+ * @return a CurveBuilderAtControlPoint1 a part of the curve builder system
+ */
+ public CurveBuilderAtControlPoint1 controlPoint1( float x, float y ){
+ return controlPoint1(new Vector2f(x,y));
+ }
+
+ /**
+ * Adds a point that the curve will attempt to move towards (but may not actually touch)
+ *
+ * The 2 control points are used to define a cubic Bézier-like curve between 2 anchors
+ * @param nextControlPoint the control point
+ * @return a CurveBuilderAtControlPoint1 a part of the curve builder system
+ */
+ public CurveBuilderAtControlPoint1 controlPoint1( Vector2f nextControlPoint ){
+ checkReuse();
+ return new CurveBuilderAtControlPoint1(curveBeingBuilt, controlPointIn, currentAnchor, nextControlPoint);
+ }
+
+ /**
+ * Produces a straight line between 2 anchor points
+ * @param x the x of the next anchor point
+ * @param y the y of the next anchor point
+ * @return a CurveBuilderAtAnchor a part of the curve builder system
+ */
+ public CurveBuilderAtAnchor anchorPoint(float x, float y){
+ return anchorPoint(new Vector2f(x,y));
+ }
+
+ /**
+ * Produces a straight line between 2 anchor points
+ * @param nextAnchor the next anchor point
+ * @return a CurveBuilderAtAnchor a part of the curve builder system
+ */
+ public CurveBuilderAtAnchor anchorPoint(Vector2f nextAnchor ){
+ //no checkReuse() as the call to controlPoint1 will do that
+ //simulate a straight line using a Bézier-like curve
+ Vector2f midOne = currentAnchor.mult(2f/3).add(nextAnchor.mult(1f/3));
+ Vector2f midTwo = currentAnchor.mult(1f/3).add(nextAnchor.mult(2f/3));
+ return controlPoint1(midOne).controlPoint2(midTwo).anchorPoint(nextAnchor);
+ }
+
+ public Curve build(){
+ checkReuse();
+ curveBeingBuilt.addControlPoint(controlPointIn, currentAnchor, null);
+ return curveBeingBuilt;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtControlPoint1.java b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtControlPoint1.java
new file mode 100644
index 0000000..9afa29a
--- /dev/null
+++ b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtControlPoint1.java
@@ -0,0 +1,38 @@
+package com.epaga.particles.valuetypes.curvebuilder;
+
+import com.epaga.particles.valuetypes.Curve;
+import com.jme3.math.Vector2f;
+
+public class CurveBuilderAtControlPoint1 extends CurveBuilderPiece{
+
+ Curve curveBeingBuilt;
+
+ public CurveBuilderAtControlPoint1(Curve curveBeingBuilt, Vector2f controlPointIn, Vector2f currentAnchor, Vector2f controlPointOut){
+ this.curveBeingBuilt = curveBeingBuilt;
+ this.curveBeingBuilt.addControlPoint(controlPointIn, currentAnchor, controlPointOut);
+ }
+
+ /**
+ * Adds a point that the curve will attempt to move towards (but may not actually touch).
+ *
+ * The 2 control points are used to define a cubic Bézier-like curve between 2 anchors
+ * @param x the control point's x
+ * @param y the control point's y
+ * @return a CurveBuilderAtControlPoint1 a part of the curve builder system
+ */
+ public CurveBuilderAtControlPoint2 controlPoint2( float x, float y ){
+ return controlPoint2(new Vector2f(x, y));
+ }
+
+ /**
+ * Adds a point that the curve will attempt to move towards (but may not actually touch).
+ *
+ * The 2 control points are used to define a cubic Bézier-like curve between 2 anchors
+ * @param nextControlPoint the control point
+ * @return a CurveBuilderAtControlPoint1 a part of the curve builder system
+ */
+ public CurveBuilderAtControlPoint2 controlPoint2( Vector2f nextControlPoint ){
+ checkReuse();
+ return new CurveBuilderAtControlPoint2(curveBeingBuilt, nextControlPoint);
+ }
+}
diff --git a/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtControlPoint2.java b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtControlPoint2.java
new file mode 100644
index 0000000..5caace0
--- /dev/null
+++ b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderAtControlPoint2.java
@@ -0,0 +1,39 @@
+package com.epaga.particles.valuetypes.curvebuilder;
+
+import com.epaga.particles.valuetypes.Curve;
+import com.jme3.math.Vector2f;
+
+public class CurveBuilderAtControlPoint2 extends CurveBuilderPiece{
+
+ Curve curveBeingBuilt;
+ Vector2f inControlPoint;
+
+ public CurveBuilderAtControlPoint2(Curve curveBeingBuilt, Vector2f inControlPoint){
+ this.curveBeingBuilt = curveBeingBuilt;
+ this.inControlPoint = inControlPoint;
+ }
+
+ /**
+ * Adds a point that the curve go through.
+ *
+ * Anchors are the starts and ends of cubic Bézier-like curves
+ * @param x the anchor point's x
+ * @param y the anchor point's y
+ * @return a CurveBuilderAtAnchor a part of the curve builder system
+ */
+ public CurveBuilderAtAnchor anchorPoint(float x, float y){
+ return anchorPoint(new Vector2f(x, y));
+ }
+
+ /**
+ * Adds a point that the curve will go through.
+ *
+ * Anchors are the starts and ends of cubic Bézier-like curves
+ * @param nextAnchor the anchor point
+ * @return a CurveBuilderAtAnchor a part of the curve builder system
+ */
+ public CurveBuilderAtAnchor anchorPoint(Vector2f nextAnchor ){
+ checkReuse();
+ return new CurveBuilderAtAnchor(curveBeingBuilt, inControlPoint, nextAnchor);
+ }
+}
diff --git a/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderPiece.java b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderPiece.java
new file mode 100644
index 0000000..ff8638c
--- /dev/null
+++ b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderPiece.java
@@ -0,0 +1,13 @@
+package com.epaga.particles.valuetypes.curvebuilder;
+
+public class CurveBuilderPiece{
+
+ boolean used = false;
+
+ protected void checkReuse(){
+ if (used){
+ throw new IllegalStateException("Curve builders must not be reused (As they actually build a single curve as they go along)");
+ }
+ used = true;
+ }
+}
diff --git a/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderStart.java b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderStart.java
new file mode 100644
index 0000000..e275066
--- /dev/null
+++ b/src/main/java/com/epaga/particles/valuetypes/curvebuilder/CurveBuilderStart.java
@@ -0,0 +1,24 @@
+package com.epaga.particles.valuetypes.curvebuilder;
+
+import com.epaga.particles.valuetypes.Curve;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+
+public class CurveBuilderStart extends CurveBuilderPiece{
+
+ Curve curveBeingBuilt = new Curve();
+
+ public CurveBuilderAtAnchor anchorPoint(float x, float y){
+ return anchorPoint(new Vector2f(x,y));
+ }
+
+ /**
+ * Adds the first anchor point, where the line will start
+ * @return CurveBuilderAtAnchor a part of the curve builder system
+ */
+ public CurveBuilderAtAnchor anchorPoint(Vector2f start){
+ checkReuse();
+ return new CurveBuilderAtAnchor(curveBeingBuilt, null, start);
+ }
+
+}
diff --git a/src/test/java/com/epaga/particles/valuetypes/CurveTest.java b/src/test/java/com/epaga/particles/valuetypes/CurveTest.java
new file mode 100644
index 0000000..0645133
--- /dev/null
+++ b/src/test/java/com/epaga/particles/valuetypes/CurveTest.java
@@ -0,0 +1,85 @@
+package com.epaga.particles.valuetypes;
+
+import com.epaga.particles.valuetypes.curvebuilder.CurveBuilderAtAnchor;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class CurveTest{
+
+ @Test
+ public void builder_straightLine(){
+ Curve curve = Curve.builder()
+ .anchorPoint(0,0)
+ .anchorPoint(1,10)
+ .build();
+
+ assertEquals(0, curve.getValue(0f), 0.001);
+ assertEquals(4, curve.getValue(0.4f), 0.001);
+ assertEquals(10, curve.getValue(1f), 0.001);
+ }
+
+ /**
+ * Tests that 2 straight lines joined together functions correctly
+ */
+ @Test
+ public void builder_doubleStraightLine(){
+ Curve curve = Curve.builder()
+ .anchorPoint(0,0)
+ .anchorPoint(0.4f,10)
+ .anchorPoint(1f, 10)
+ .build();
+
+ assertEquals(0, curve.getValue(0f), 0.001);
+ assertEquals(5, curve.getValue(0.2f), 0.001);
+ assertEquals(10, curve.getValue(0.8f), 0.001);
+ }
+
+ /**
+ * Tests that a Bézier-like curve functions correctly
+ *
+ * (Its not actually a true Bézier curve becuse a Bézier curve can "go backwards" and follows a
+ * slightly different path
+ */
+ @Test
+ public void builder_curve(){
+
+ Curve curve = Curve.builder()
+ .anchorPoint(0,0)
+ .controlPoint1(0.2f, 1)
+ .controlPoint2(0.8f, 0)
+ .anchorPoint(1,1)
+ .build();
+
+ //expected values obtained using https://www.desmos.com/calculator/ebdtbxgbq0
+
+ assertEquals(0, curve.getValue(0f), 0.001);
+
+ //value obtained as 0.1 along using the following
+ // along line 1 = 0.9 * 0 + 0.1 * 1 = 0.1
+ // along line 2 = 0.9 * 1 + 0.1 * 0 = 0.9
+ // along line 3 = 0.9 * 0 + 0.1 * 1 = 0.1
+
+ //obtain 2 new lines between along line 1 -> along line 2 and along line 2 -> along line 3. Get 0.1 along each one
+ //along second order 1 = 0.9 * 0.1 + 0.1 * 0.9 = 0.18
+ //along second order 2 = 0.9 * 0.9 + 0.1 * 0.1 = 0.82
+
+ //final result is 0.1 along the line between the second order points
+ // 0.9 * 0.18 + 0.1 * 0.82
+
+ assertEquals(0.244, curve.getValue(0.1f), 0.001);
+
+ assertEquals(0.5, curve.getValue(0.5f), 0.001);
+ assertEquals(1, curve.getValue(1), 0.001);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void builder_reuseLeadsToException(){
+ CurveBuilderAtAnchor builder = Curve.builder()
+ .anchorPoint(0,0);
+
+ Curve legalUse = builder.build();
+ Curve illegalReuse = builder.build();
+ }
+
+}