Skip to content

Instantly share code, notes, and snippets.

@Barteks2x
Created December 22, 2016 17:53
Show Gist options
  • Save Barteks2x/6c9c0357223d49f64258ce9660f8eebd to your computer and use it in GitHub Desktop.
Save Barteks2x/6c9c0357223d49f64258ce9660f8eebd to your computer and use it in GitHub Desktop.
package cubicchunks.worldgen.gui;
import com.google.common.base.Converter;
import java.util.HashSet;
import java.util.Set;
import java.util.function.DoubleUnaryOperator;
import javax.annotation.ParametersAreNonnullByDefault;
import mcp.MethodsReturnNonnullByDefault;
import static cubicchunks.util.MathUtil.lerp;
import static cubicchunks.util.MathUtil.unlerp;
import static java.lang.Math.abs;
import static java.lang.Math.log;
import static java.lang.Math.max;
import static java.lang.Math.pow;
import static java.lang.Math.round;
import static net.minecraft.util.math.MathHelper.ceil;
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
public class ExponentialConverter extends Converter<Float, Float> {
private final boolean hasZero;
private final float minExpPos;
private final float maxExpPos;
private final float minExpNeg;
private final float maxExpNeg;
private final Set<SnapDataEntry> snapData;
private final float baseValue;
private final double zeroPos;
private final double positiveExpStart;
private final double negativeExpStart;
private final DoubleUnaryOperator snapRadius;
private final double minLinearPosVal;
private final double minLinearNegVal;
private ExponentialConverter(Builder builder) {
boolean hasPositivePart = !Float.isNaN(builder.minExpPos) && !Double.isNaN(builder.maxExpPos);
boolean hasNegativePart = !Float.isNaN(builder.minExpNeg) && !Double.isNaN(builder.maxExpNeg);
if (!hasPositivePart) {
builder.minExpPos = 0;
builder.maxExpPos = 0;
}
if (!hasNegativePart) {
builder.minExpNeg = 0;
builder.maxExpNeg = 0;
}
this.hasZero = builder.hasZero;
this.minExpNeg = builder.minExpNeg;
this.maxExpNeg = builder.maxExpNeg;
this.minExpPos = builder.minExpPos;
this.maxExpPos = builder.maxExpPos;
this.baseValue = builder.baseVal;
this.snapData = builder.snapData;
this.snapRadius = builder.snapRadius;
// find zeroX, minPosX, maxNegX
// to solve:
//
// d/dposMinX(linearPartPositive(x, negMaxX, posMinX, zeroX)) = d/dposMinX(exponentialPartPositive(x, negMaxX, posMinX)) for x=posMinX
// d/dnegMaxX(linearPartNegative(x, negMaxX, posMinX, zeroX)) = d/dnegMaxX(exponentialPartNegative(x, negMaxX, posMinX)) for x=negMaxX
// (1-posMinX)/(maxPositiveExponent-minPositiveExponent) = negMaxX/(maxNegativeExponent-minNegativeExponent)
// ^ here I did a mistake, it's supposed to be division. I'm ot redoing it all from the scratch, I will just do result = 1 - result
//
// solve for: posMinX, negMaxX, zeroX
//
// linearPartPositive(x, negMaxX, posMinX, zeroX) =
// = [(x - zeroX)/(posMinX - zeroX)]*(baseValue^minExpPos)
// linearPartNegative(x, negMaxX, posMinX, zeroX) =
// = [(x - negMaxX)/(zeroX - negMaxX)]*(baseValue^minExpNeg) - baseValue^minExpNeg
//
// exponentialPartPositive(x, negMaxX, posMinX) =
// = baseValue^[(x - posMinX)/(1 - posMinX)*(maxExpPos-minExpPos)+minExpPos]
// exponentialPartNegative(x, negMaxX, posMinX) =
// = -1*baseValue^[[1 - (x - negMinX)/(negMaxX - negMinX)]*(maxExpNeg-minExpNeg)+minExpNeg]
//
// Now the ugly part: I don't want to solve it by hand,
// so I need to turn it into format that I can put into wolfram alpha:
//
// x -> x
// zeroX -> z
// posMinX -> p
// negMaxX -> n
// baseValue-> b
// minExpPos-> c
// maxExpPos-> d
// minExpNeg-> f //no e, because e is constant
// maxExpNeg-> g
//
// d/dx(((x - z)/(p - z))*(b^c)) = d/dx(b^((x - p)/(1 - p)*(d-c)+c)) for x == p
// d/dx(((x - n)/(z - n))*(b^f) - b^f) = d/dx(-1*b^((1 - x/n)*(g-f)+f)) for x == n
// (1-p)/(d-c) = n/(g-f)
//
// now if we put it into wolfram alpha... we find out that wolfram alpha refuses to cooperate.
// so let's try in smaller pieces. First get rid of derivatives and turn it into wolfram language:
//
// b^c/(p - z) == (b^(c + ((-c + d) (-p + x))/(1 - p)) (-c + d) Log[b])/(1 - p) for x == p
// b^f/(-n + z) == (b^(f + (-f + g) (1 - x/n)) (-f + g) Log[b])/n for x == n
// (1 - p)/(d - c) == n/(g - f)
//
// b^c/(p - z) == (b^(c + ((-c + d) (-p + p))/(1 - p)) (-c + d) Log[b])/(1 - p) <==> b^c/(p - z) == (b^c (-c + d) Log[b])/(1 - p)
// b^f/(-n + z) == (b^(f + (-f + g) (1 - n/n)) (-f + g) Log[b])/n <==> b^f/(-n + z) == (b^f (-f + g) Log[b])/n
// (1 - p)/(d - c) == n/(g - f)
//
// b^c/(p - z) == (b^c (-c + d) Log[b])/(1 - p) -- substitute p from below and solve for z --> z == (-1 + f Log[b] - g Log[b])/(-2 + c Log[b] - d Log[b] + f Log[b] - g Log[b])
// b^f/(-n + z) == (b^f (-f + g) Log[b])/n -- substitute n from below and solve for p --> p == ((-1 + (f - g - c z + d z) Log[b])/(-1 + (f - g) Log[b]))
// (1 - p)/(d - c) == n/(g - f) -- solve for n --> n == (-(((f - g) (-1 + p))/(c - d)))
//
// So the solution for z is:
//
// z == (-1 + f Log[b] - g Log[b])/(-2 + c Log[b] - d Log[b] + f Log[b] - g Log[b])
//
// Now that we have z we can express p and n in terms of it:
//
// p == (-1 + c z Log[b] - d z Log[b])/(-1 + c Log[b] - d Log[b])
// n == ((f - g)*z*Log[b])/(-1 + f*Log[b] - g*Log[b])
double b = baseValue;
double lb = log(b);
double c = minExpPos;
double d = maxExpPos;
double f = minExpNeg;
double g = maxExpNeg;
double z = (-1 + f*lb - g*lb)/(-2 + c*lb - d*lb + f*lb - g*lb);
if (!hasNegativePart && hasPositivePart) {
z = 0;
} else if (hasNegativePart && !hasPositivePart) {
z = 1;
} else if (!hasNegativePart || !hasPositivePart) {
throw new IllegalArgumentException("Slider must have at least either positive or negative part");
}
double p = (-1 + c*z*lb - d*z*lb)/(-1 + c*lb - d*lb);
double n = ((f - g)*z*lb)/(-1 + f*lb - g*lb);
this.zeroPos = z;
this.positiveExpStart = hasZero ? p : z;
this.negativeExpStart = hasZero ? n : z;
this.minLinearPosVal = hasPositivePart ? pow(baseValue, minExpPos) : Double.POSITIVE_INFINITY;
this.minLinearNegVal = hasNegativePart ? -pow(baseValue, minExpNeg) : Double.NEGATIVE_INFINITY;
}
@Override protected Float doForward(Float slideVal) {
double max = -Double.MAX_VALUE;
double best = 0;
final double rawValue = doForwardsRaw(slideVal);
int startExponent = getMaxExp();
for (SnapDataEntry e : snapData) {
for (int trySnapDivExp = startExponent; ; trySnapDivExp--) {
double snapVal = getSnapValue(rawValue, trySnapDivExp, e);
double snapSliderVal = doBackwardDouble(snapVal);
double snapDistance = abs(slideVal - snapSliderVal);
if (Double.isNaN(snapVal) || Double.isInfinite(snapVal) || snapDistance < getSnapRadius(getDivisor(trySnapDivExp, e))) {
double v = getDivisor(trySnapDivExp, e);
if (v > max) {
max = v;
best = snapVal;
}
break;
}
}
}
return (float) best;
}
private double getDivisor(int trySnapDivExp, SnapDataEntry e) {
return pow(e.baseVal, trySnapDivExp)*e.multiplier;
}
private double getSnapValue(double rawValue, int exp, SnapDataEntry e) {
double divisor = getDivisor(exp, e);
if (divisor == 0) {
return Double.NaN;
}
return round(rawValue/divisor)*divisor;
}
private double getSnapRadius(double at) {
return this.snapRadius.applyAsDouble(at);
}
private int getMaxExp() {
return ceil(max(maxExpNeg, maxExpPos));
}
private double doForwardsRaw(double x) {
// for x below negative start - negative exponential
// for x between negMaxX and posMinX - linear
// for x above posMinX - positive exponential
double negMinX = 0;
double negMaxX = getNegStartX();
double zeroX = getZeroX();
double posMinX = getPosStartX();
double posMaxX = 1;
if (x < negMaxX) {
// scale x to be between 0 and 1 (undo linear interpolation) and reverse order for negatives, so that values closer to 1 are further away from 0
x = 1 - unlerp(x, negMinX, negMaxX);
// interpolate for exponents
x = lerp(x, minExpNeg, maxExpNeg);
return -pow(baseValue, x);
}
if (x > posMinX) {
// scale x to be between 0 and 1 (undo linear interpolation)
x = unlerp(x, posMinX, posMaxX);
x = lerp(x, minExpPos, maxExpPos);
return pow(baseValue, x);
}
// handle the linear part
if (x < zeroX) {
// negative linear part
double minNegXLin = negMaxX;
double maxNegXLin = zeroX;
x = unlerp(x, minNegXLin, maxNegXLin);
return lerp(x, minLinearNegVal, 0);
} else {
// positive linear part
double minPosXLin = zeroX;
double maxPosXLin = posMinX;
x = unlerp(x, minPosXLin, maxPosXLin);
return lerp(x, 0, minLinearPosVal);
}
}
private double getPosStartX() {
return positiveExpStart;
}
private double getZeroX() {
return zeroPos;
}
private double getNegStartX() {
return negativeExpStart;
}
@Override protected Float doBackward(Float aFloat) {
return (float) doBackwardDouble(aFloat);
}
private double doBackwardDouble(double value) {
// linear part for positive
if (value >= 0 && value <= minLinearPosVal) {
/*
* Code to reverse:
* double minPosXLin = zeroX;
* double maxPosXLin = posMinX;
* x = unlerp(x, minPosXLin, maxPosXLin);
* return lerp(x, 0, minLinearPosVal);
*/
double maxPosXLin = getPosStartX();
double minPosXLin = getZeroX();
double x = unlerp(value, 0, minLinearPosVal);
x = lerp(x, minPosXLin, maxPosXLin);
return x;
}
// linear part for negative
if (value <= 0 && value >= minLinearNegVal) {
/*
* Code to reverse:
* double minNegXLin = negMaxX;
* double maxNegXLin = zeroX;
* x = unlerp(x, minNegXLin, maxNegXLin);
* return lerp(x, minLinearNegVal, 0);
*/
double minNegXLin = getNegStartX();
double maxNegXLin = getZeroX();
double x = unlerp(value, minLinearNegVal, 0);
x = lerp(x, minNegXLin, maxNegXLin);
return x;
}
double negMinX = 0;
double negMaxX = getNegStartX();
double posMinX = getPosStartX();
double posMaxX = 1;
if (value >= minLinearPosVal) {
/*
* Code to reverse:
* x = unlerp(x, posMinX, posMaxX);
* x = lerp(x, minExpPos, maxExpPos);
* return pow(baseValue, x);
*/
double x = log(value)/log(baseValue);
x = unlerp(x, minExpPos, maxExpPos);
x = lerp(x, posMinX, posMaxX);
return x;
} else {
assert value <= minLinearNegVal;
/*
* Code to reverse:
* // x = 1 - unlerp(x, negMinX, negMaxX); --> equivalent
* x = unlerp(x, negMinX, negMaxX);
* x = 1 - x;
* x = lerp(x, minExpNeg, maxExpNeg);
* return -pow(baseValue, x);
*/
double x = log(-value)/log(baseValue);
x = unlerp(x, minExpNeg, maxExpNeg);
x = 1 - x;
x = lerp(x, negMinX, negMaxX);
return x;
}
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private boolean hasZero = false;
private float minExpPos = Float.NaN;
private float maxExpPos = Float.NaN;
private float minExpNeg = Float.NaN;
private float maxExpNeg = Float.NaN;
private float baseVal;
public Set<SnapDataEntry> snapData = new HashSet<>();
private DoubleUnaryOperator snapRadius;
public Builder setHasZero(boolean hasZero) {
this.hasZero = hasZero;
return this;
}
public Builder setPositiveExponentRange(float min, float max) {
minExpPos = min;
maxExpPos = max;
return this;
}
public Builder setNegativeExponentRange(float min, float max) {
minExpNeg = min;
maxExpNeg = max;
return this;
}
public Builder setBaseValue(float baseVal) {
this.baseVal = baseVal;
return this;
}
public Builder setSnapRadiusFunction(DoubleUnaryOperator op) {
this.snapRadius = op;
return this;
}
public ExponentialConverter build() {
this.snapData.add(new SnapDataEntry(baseVal, 1));
return new ExponentialConverter(this);
}
}
private static final class SnapDataEntry {
private final double baseVal, multiplier;
private SnapDataEntry(double baseVal, double multiplier) {
this.baseVal = baseVal;
this.multiplier = multiplier;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SnapDataEntry that = (SnapDataEntry) o;
if (Double.compare(that.baseVal, baseVal) != 0) return false;
return Double.compare(that.multiplier, multiplier) == 0;
}
@Override public int hashCode() {
int result;
long temp;
temp = Double.doubleToLongBits(baseVal);
result = (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(multiplier);
result = 31*result + (int) (temp ^ (temp >>> 32));
return result;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment