Code style cleanup from #12.

- Moved all curve-related classes into a new package.
- Added some fields and methods to Curve abstract class.
- Removed the old (no longer used) Bezier subclass.
- Changed Error throwing to ErrorHandler.error() calls.
- Formatted code.

Also fixed a crash when reaching the ranking screen with the "Auto" mod active.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han
2015-01-29 20:36:23 -05:00
parent 94d7ff37eb
commit e93fe25834
14 changed files with 741 additions and 734 deletions

View File

@@ -0,0 +1,121 @@
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.objects.curves;
/**
* Representation of a Bezier curve with the distance between each point calculated.
*/
public class Bezier2 {
/** The control points of the Bezier curve. */
private Vec2f[] points;
/** Points along the curve of the Bezier curve. */
private Vec2f[] curve;
/** Distances between a point of the curve and the last point. */
private float[] curveDis;
/** The number of points along the curve. */
private int ncurve;
/** The total distances of this Bezier. */
private float totalDistance;
/**
* Constructor.
* @param points the control points
*/
public Bezier2(Vec2f[] points) {
this.points = points;
// approximate by finding the length of all points
// (which should be the max possible length of the curve)
float approxlength = 0;
for (int i = 0; i < points.length - 1; i++)
approxlength += points[i].cpy().sub(points[i + 1]).len();
// subdivide the curve
this.ncurve = (int) (approxlength / 4);
this.curve = new Vec2f[ncurve];
for (int i = 0; i < ncurve; i++)
curve[i] = pointAt(i / (float) ncurve);
// find the distance of each point from the previous point
this.curveDis = new float[ncurve];
this.totalDistance = 0;
for (int i = 0; i < ncurve; i++) {
curveDis[i] = (i == 0) ? 0 : curve[i].cpy().sub(curve[i - 1]).len();
totalDistance += curveDis[i];
}
// System.out.println("New Bezier2 "+points.length+" "+approxlength+" "+totalDistance());
}
/**
* Returns the point on the Bezier curve at a value t.
* @param t the t value [0, 1]
* @return the point [x, y]
*/
public Vec2f pointAt(float t) {
Vec2f c = new Vec2f();
int n = points.length - 1;
for (int i = 0; i <= n; i++) {
c.x += points[i].x * bernstein(i, n, t);
c.y += points[i].y * bernstein(i, n, t);
}
return c;
}
/**
* Returns the points along the curve of the Bezier curve.
*/
public Vec2f[] getCurve() { return curve; }
/**
* Returns the distances between a point of the curve and the last point.
*/
public float[] getCurveDistances() { return curveDis; }
/**
* Returns the number of points along the curve.
*/
public int points() { return ncurve; }
/**
* Returns the total distances of this Bezier curve.
*/
public float totalDistance() { return totalDistance; }
/**
* Calculates the factorial of a number.
*/
private static long factorial(int n) {
return (n <= 1 || n > 20) ? 1 : n * factorial(n - 1);
}
/**
* Calculates the Bernstein polynomial.
* @param i the index
* @param n the degree of the polynomial (i.e. number of points)
* @param t the t value [0, 1]
*/
private static double bernstein(int i, int n, float t) {
return factorial(n) / (factorial(i) * factorial(n - i)) *
Math.pow(t, i) * Math.pow(1 - t, n - i);
}
}

View File

@@ -0,0 +1,188 @@
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.Utils;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
/**
* Representation of a curve along a Circumscribed Circle of three points.
* http://en.wikipedia.org/wiki/Circumscribed_circle
*/
public class CircumscribedCircle extends Curve {
/** PI constants. */
private static final float
TWO_PI = (float) (Math.PI * 2),
HALF_PI = (float) (Math.PI / 2);
/** The center of the Circumscribed Circle. */
private Vec2f circleCenter;
/** The radius of the Circumscribed Circle. */
private float radius;
/** The three points to create the Circumscribed Circle from. */
private Vec2f start, mid, end;
/** The three angles relative to the circle center. */
private float startAng, endAng, midAng;
/** The start and end angles for drawing. */
private float drawStartAngle, drawEndAngle;
/** The number of steps in the curve to draw. */
private float step;
/**
* Constructor.
* @param hitObject the associated OsuHitObject
* @param color the color of this curve
*/
public CircumscribedCircle(OsuHitObject hitObject, Color color) {
super(hitObject, color);
this.step = hitObject.getPixelLength() / 5f;
// construct the three points
this.start = new Vec2f(getX(0), getY(0));
this.mid = new Vec2f(getX(1), getY(1));
this.end = new Vec2f(getX(2), getY(2));
// find the circle center
Vec2f mida = start.midPoint(mid);
Vec2f midb = end.midPoint(mid);
Vec2f nora = mid.cpy().sub(start).nor();
Vec2f norb = mid.cpy().sub(end).nor();
this.circleCenter = intersect(mida, nora, midb, norb);
if (circleCenter == null)
return;
// find the angles relative to the circle center
Vec2f startAngPoint = start.cpy().sub(circleCenter);
Vec2f midAngPoint = mid.cpy().sub(circleCenter);
Vec2f endAngPoint = end.cpy().sub(circleCenter);
this.startAng = (float) Math.atan2(startAngPoint.y, startAngPoint.x);
this.midAng = (float) Math.atan2(midAngPoint.y, midAngPoint.x);
this.endAng = (float) Math.atan2(endAngPoint.y, endAngPoint.x);
// find the angles that pass through midAng
if (!isIn(startAng, midAng, endAng)) {
if (Math.abs(startAng + TWO_PI - endAng) < TWO_PI && isIn(startAng + (TWO_PI), midAng, endAng))
startAng += TWO_PI;
else if (Math.abs(startAng - (endAng + TWO_PI)) < TWO_PI && isIn(startAng, midAng, endAng + (TWO_PI)))
endAng += TWO_PI;
else if (Math.abs(startAng - TWO_PI - endAng) < TWO_PI && isIn(startAng - (TWO_PI), midAng, endAng))
startAng -= TWO_PI;
else if (Math.abs(startAng - (endAng - TWO_PI)) < TWO_PI && isIn(startAng, midAng, endAng - (TWO_PI)))
endAng -= TWO_PI;
else {
ErrorHandler.error(
String.format("Cannot find angles between midAng (%.3f %.3f %.3f).",
startAng, midAng, endAng), null, true
);
return;
}
}
// find an angle with an arc length of pixelLength along this circle
this.radius = startAngPoint.len();
float pixelLength = hitObject.getPixelLength() * OsuHitObject.getXMultiplier();
float arcAng = pixelLength / radius; // len = theta * r / theta = len / r
// now use it for our new end angle
this.endAng = (endAng > startAng) ? startAng + arcAng : startAng - arcAng;
// finds the angles to draw for repeats
this.drawEndAngle = (float) ((endAng + (startAng > endAng ? HALF_PI : -HALF_PI)) * 180 / Math.PI);
this.drawStartAngle = (float) ((startAng + (startAng > endAng ? -HALF_PI : HALF_PI)) * 180 / Math.PI);
}
/**
* Checks to see if "b" is between "a" and "c"
* @return true if b is between a and c
*/
private boolean isIn(float a, float b, float c) {
return (b > a && b < c) || (b < a && b > c);
}
/**
* Finds the point of intersection between the two parametric lines
* {@code A = a + ta*t} and {@code B = b + tb*u}.
* http://gamedev.stackexchange.com/questions/44720/
* @param a the initial position of the line A
* @param ta the direction of the line A
* @param b the initial position of the line B
* @param tb the direction of the line B
* @return the point at which the two lines intersect
*/
private Vec2f intersect(Vec2f a, Vec2f ta, Vec2f b, Vec2f tb) {
// xy = a + ta * t = b + tb * u
// t =(b + tb*u -a)/ta
//t(x) == t(y)
//(b.x + tb.x*u -a.x)/ta.x = (b.y + tb.y*u -a.y)/ta.y
// b.x*ta.y + tb.x*u*ta.y -a.x*ta.y = b.y*ta.x + tb.y*u*ta.x -a.y*ta.x
// tb.x*u*ta.y - tb.y*u*ta.x= b.y*ta.x -a.y*ta.x -b.x*ta.y +a.x*ta.y
//u *(tb.x*ta.y - tb.y*ta.x) = (b.y-a.y)ta.x +(a.x-b.x)ta.y
//u = ((b.y-a.y)ta.x +(a.x-b.x)ta.y) / (tb.x*ta.y - tb.y*ta.x);
float des = tb.x * ta.y - tb.y * ta.x;
if (Math.abs(des) < 0.00001f) {
ErrorHandler.error("Vectors are parallel.", null, true);
return null;
}
float u = ((b.y - a.y) * ta.x + (a.x - b.x) * ta.y) / des;
return b.cpy().add(tb.x * u, tb.y * u);
}
@Override
public float[] pointAt(float t) {
float ang = lerp(startAng, endAng, t);
return new float[] {
(float) (Math.cos(ang) * radius + circleCenter.x),
(float) (Math.sin(ang) * radius + circleCenter.y)
};
}
@Override
public void draw() {
Image hitCircle = GameImage.HITCIRCLE.getImage();
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
for (int i = 0; i < step; i++) {
float[] xy = pointAt(i / step);
Utils.drawCentered(hitCircleOverlay, xy[0], xy[1], Utils.COLOR_WHITE_FADE);
}
for (int i = 0; i < step; i++) {
float[] xy = pointAt(i / step);
Utils.drawCentered(hitCircle, xy[0], xy[1], color);
}
}
@Override
public float getEndAngle() { return drawEndAngle; }
@Override
public float getStartAngle() { return drawStartAngle; }
}

View File

@@ -0,0 +1,97 @@
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.OsuHitObject;
import org.newdawn.slick.Color;
/**
* Representation of a curve.
*/
public abstract class Curve {
/** The associated OsuHitObject. */
protected OsuHitObject hitObject;
/** The color of this curve. */
protected Color color;
/**
* Constructor.
* @param hitObject the associated OsuHitObject
* @param color the color of this curve
*/
protected Curve(OsuHitObject hitObject, Color color) {
this.hitObject = hitObject;
this.color = color;
}
/**
* Returns the point on the curve at a value t.
* @param t the t value [0, 1]
* @return the point [x, y]
*/
public abstract float[] pointAt(float t);
/**
* Draws the full curve to the graphics context.
*/
public abstract void draw();
/**
* Returns the angle of the first control point.
*/
public abstract float getEndAngle();
/**
* Returns the angle of the last control point.
*/
public abstract float getStartAngle();
/**
* Returns the x coordinate of the control point at index i.
*/
protected float getX(int i) {
return (i == 0) ? hitObject.getX() : hitObject.getSliderX()[i - 1];
}
/**
* Returns the y coordinate of the control point at index i.
*/
protected float getY(int i) {
return (i == 0) ? hitObject.getY() : hitObject.getSliderY()[i - 1];
}
/**
* Linear interpolation of a and b at t.
*/
protected float lerp(float a, float b, float t) {
return a * (1 - t) + b * t;
}
/**
* A recursive method to evaluate polynomials in Bernstein form or Bezier curves.
* http://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
*/
protected float deCasteljau(float[] a, int i, int order, float t) {
if (order == 0)
return a[i];
return lerp(deCasteljau(a, i, order - 1, t), deCasteljau(a, i + 1, order - 1, t), t);
}
}

View File

@@ -0,0 +1,175 @@
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.Utils;
import java.util.Iterator;
import java.util.LinkedList;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
/**
* Representation of a Bezier curve with equidistant points.
* http://pomax.github.io/bezierinfo/#tracing
*/
public class LinearBezier extends Curve {
/** The angles of the first and last control points for drawing. */
private float startAngle, endAngle;
/** List of Bezier curves in the set of points. */
private LinkedList<Bezier2> beziers = new LinkedList<Bezier2>();
/** Points along the curve at equal distance. */
private Vec2f[] curve;
/** The number of points along the curve. */
private int ncurve;
/**
* Constructor.
* @param hitObject the associated OsuHitObject
* @param color the color of this curve
*/
public LinearBezier(OsuHitObject hitObject, Color color) {
super(hitObject, color);
// splits points into different Beziers if has the same points (red points)
int controlPoints = hitObject.getSliderX().length + 1;
LinkedList<Vec2f> points = new LinkedList<Vec2f>(); // temporary list of points to separate different Bezier curves
Vec2f lastPoi = null;
for (int i = 0; i < controlPoints; i++) {
Vec2f tpoi = new Vec2f(getX(i), getY(i));
if (lastPoi != null && tpoi.equals(lastPoi)) {
if (points.size() >= 2)
beziers.add(new Bezier2(points.toArray(new Vec2f[0])));
points.clear();
}
points.add(tpoi);
lastPoi = tpoi;
}
if (points.size() < 2) {
// trying to continue Bezier with less than 2 points
// probably ending on a red point, just ignore it
} else {
beziers.add(new Bezier2(points.toArray(new Vec2f[0])));
points.clear();
}
// find the length of all beziers
// int totalDistance = 0;
// for (Bezier2 bez : beziers) {
// totalDistance += bez.totalDistance();
// }
// now try to creates points the are equidistant to each other
this.ncurve = (int) (hitObject.getPixelLength() / 5f);
this.curve = new Vec2f[ncurve + 1];
float distanceAt = 0;
Iterator<Bezier2> iter = beziers.iterator();
int curPoint = 0;
Bezier2 curBezier = iter.next();
Vec2f lastCurve = curBezier.getCurve()[0];
float lastDistanceAt = 0;
// length of Bezier should equal pixel length (in 640x480)
float pixelLength = hitObject.getPixelLength() * OsuHitObject.getXMultiplier();
// for each distance, try to get in between the two points that are between it
for (int i = 0; i < ncurve + 1; i++) {
int prefDistance = (int) (i * pixelLength / ncurve);
while (distanceAt < prefDistance) {
lastDistanceAt = distanceAt;
lastCurve = curBezier.getCurve()[curPoint];
distanceAt += curBezier.getCurveDistances()[curPoint++];
if (curPoint >= curBezier.points()) {
if (iter.hasNext()) {
curBezier = iter.next();
curPoint = 0;
} else
curPoint = curBezier.points() - 1;
}
}
Vec2f thisCurve = curBezier.getCurve()[curPoint];
// interpolate the point between the two closest distances
if (distanceAt - lastDistanceAt > 1) {
float t = (prefDistance - lastDistanceAt) / (distanceAt - lastDistanceAt);
curve[i] = new Vec2f(lerp(lastCurve.x, thisCurve.x, t), lerp(lastCurve.y, thisCurve.y, t));
// System.out.println("Dis "+i+" "+prefDistance+" "+lastDistanceAt+" "+distanceAt+" "+curPoint+" "+t);
} else
curve[i] = thisCurve;
}
// if (hitObject.getRepeatCount() > 1) {
Vec2f c1 = curve[0];
int cnt = 1;
Vec2f c2 = curve[cnt++];
while (c2.cpy().sub(c1).len() < 1)
c2 = curve[cnt++];
this.startAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI);
c1 = curve[ncurve - 1];
cnt = ncurve - 2;
c2 = curve[cnt];
while (c2.cpy().sub(c1).len() < 1)
c2 = curve[cnt--];
this.endAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI);
// }
// System.out.println("Total Distance: "+totalDistance+" "+distanceAt+" "+beziers.size()+" "+hitObject.getPixelLength()+" "+OsuHitObject.getXMultiplier());
}
@Override
public float[] pointAt(float t) {
float indexF = t * ncurve;
int index = (int) indexF;
if (index >= ncurve) {
Vec2f poi = curve[ncurve - 1];
return new float[] { poi.x, poi.y };
} else {
Vec2f poi = curve[index];
Vec2f poi2 = curve[index + 1];
float t2 = indexF - index;
return new float[] {
lerp(poi.x, poi2.x, t2),
lerp(poi.y, poi2.y, t2)
};
}
}
@Override
public void draw() {
Image hitCircle = GameImage.HITCIRCLE.getImage();
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
for (int i = curve.length - 2; i >= 0; i--)
Utils.drawCentered(hitCircleOverlay, curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE);
for (int i = curve.length - 2; i >= 0; i--)
Utils.drawCentered(hitCircle, curve[i].x, curve[i].y, color);
}
@Override
public float getEndAngle() { return endAngle; }
@Override
public float getStartAngle() { return startAngle; }
}

View File

@@ -0,0 +1,98 @@
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.objects.curves;
/**
* A two-dimensional floating-point vector.
*/
public class Vec2f {
/** Vector coordinates. */
public float x, y;
/**
* Constructor of the (nx, ny) vector.
*/
public Vec2f(float nx, float ny) {
x = nx;
y = ny;
}
/**
* Constructor of the (0,0) vector.
*/
public Vec2f() {}
/**
* Finds the midpoint between this vector and another vector.
* @param o the other vector
* @return a midpoint vector
*/
public Vec2f midPoint(Vec2f o) {
return new Vec2f((x + o.x) / 2, (y + o.y) / 2);
}
/**
* Subtracts a vector from this vector.
* @param o the other vector
* @return itself (for chaining)
*/
public Vec2f sub(Vec2f o) {
x -= o.x;
y -= o.y;
return this;
}
/**
* Sets this vector to the normal of this vector.
* @return itself (for chaining)
*/
public Vec2f nor() {
float nx = -y, ny = x;
x = nx;
y = ny;
return this;
}
/**
* Returns a copy of this vector.
*/
public Vec2f cpy() { return new Vec2f(x, y); }
/**
* Adds nx to the x component and ny to the y component of this vector.
* @return itself (for chaining)
*/
public Vec2f add(float nx, float ny) {
x += nx;
y += ny;
return this;
}
/**
* Returns the length of this vector.
*/
public float len() { return (float) Math.sqrt(x * x + y * y); }
/**
* Compares this vector to another vector.
* @param o the other vector
* @return true if the two vectors are numerically equal
*/
public boolean equals(Vec2f o) { return (x == o.x && y == o.y); }
}