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(); + } + +}