Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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:
* <pre>{@code
* ValueType sizeOverTime = new ValueType(Curve.builder().anchorPoint(0f, 0.03f).anchorPoint(1f, 0.01f).end());
* SizeInfluencer sizeInfluencer = new SizeInfluencer();
* sizeInfluencer.setSizeOverTime(sizeOverTime);
* }</pre>
*
* @author t0neg0d
* @author Jeddic
*/
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/epaga/particles/valuetypes/Curve.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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:
*
* <pre>{@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();
* }</pre>
*
* 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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}

}
85 changes: 85 additions & 0 deletions src/test/java/com/epaga/particles/valuetypes/CurveTest.java
Original file line number Diff line number Diff line change
@@ -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();
}

}