CircumscribedCircle, LinearBezier Curves, multi repeats arrows,

CircleColorOrdering

fd
This commit is contained in:
fd 2015-01-28 00:30:51 -05:00
parent f71b2c25f2
commit 33f2207881
3 changed files with 431 additions and 25 deletions

View File

@ -102,10 +102,17 @@ public class OsuHitObject {
* @param height the container height
*/
public static void init(int width, int height) {
xMultiplier = (width * 0.6f) / MAX_X;
yMultiplier = (height * 0.6f) / MAX_Y;
xOffset = width / 5;
yOffset = height / 5;
int swidth = width;
int sheight = height;
if(swidth*3>sheight*4){
swidth = sheight*4/3;
}else{
sheight = swidth*3/4;
}
xMultiplier = swidth / 640f; //(width * 1f) / MAX_X; // width * MAX_X/800f / MAX_X
yMultiplier = sheight / 480f;//(height * 1f) / MAX_Y;
xOffset = (int)(width - MAX_X * xMultiplier)/2 ;//width / 5; 800-512/2
yOffset = (int)(height - MAX_Y * yMultiplier)/2 ;//height / 5;
}
/**
@ -280,4 +287,8 @@ public class OsuHitObject {
* @return true if new combo
*/
public boolean isNewCombo() { return (type & TYPE_NEWCOMBO) > 0; }
}
public static float getMultiplier() {
return xMultiplier;
}
}

View File

@ -535,7 +535,7 @@ public class OsuParser {
// set combo info
// - new combo: get next combo index, reset combo number
// - else: maintain combo index, increase combo number
if (hitObject.isNewCombo()) {
if (hitObject.isNewCombo() && !hitObject.isSpinner() || objectIndex==0) {
comboIndex = (comboIndex + 1) % osu.combo.length;
comboNumber = 1;
}

View File

@ -18,6 +18,9 @@
package itdelatrisu.opsu.objects;
import java.util.Iterator;
import java.util.LinkedList;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.GameData;
@ -59,7 +62,7 @@ public class Slider implements HitObject {
private Color color;
/** The underlying Bezier object. */
private Bezier bezier;
private Curve bezier;
/** The time duration of the slider, in milliseconds. */
private float sliderTime = 0f;
@ -88,6 +91,174 @@ public class Slider implements HitObject {
/** Number of ticks hit and tick intervals so far. */
private int ticksHit = 0, tickIntervals = 1;
private abstract class Curve{
public abstract float[] pointAt(float t);
public abstract void draw();
public abstract float getEndAngle();
public abstract float getStartAngle();
}
private class Vec2f{
float x, y;
public Vec2f(float nx, float ny) {
x=nx;
y=ny;
}
public Vec2f() {
// TODO Auto-generated constructor stub
}
public Vec2f midPoint(Vec2f o){
return new Vec2f((x+o.x)/2, (y+o.y)/2);
}
public Vec2f sub(Vec2f o){
x-=o.x;
y-=o.y;
return this;
}
public Vec2f nor(){
float nx = -y, ny =x;
x=nx;
y=ny;
return this;
}
public Vec2f cpy(){
return new Vec2f(x, y);
}
public Vec2f add(float nx, float ny) {
x+=nx;
y+=ny;
return this;
}
public float len() {
return (float) Math.sqrt(x*x + y*y);
}
public boolean equals(Vec2f o){
return x==o.x && y==o.y;
}
}
//finds a circle that intersects all three points
//http://en.wikipedia.org/wiki/Circumscribed_circle
private class CircumscribedCircle extends Curve{
Vec2f circleCenter;
Vec2f start ,mid ,end;
float startAng,endAng,midAng;
float drawStartAngle,drawEndAngle;
float radius;
final float twopi = (float) (2*Math.PI);
final float halfpi = (float) (Math.PI/2);
private float step;
public CircumscribedCircle(){
this.step = hitObject.getPixelLength() / 5;
start = new Vec2f(getX(0), getY(0));
mid = new Vec2f(getX(1), getY(1));
end = new Vec2f(getX(2), getY(2));
Vec2f mida = start.midPoint(mid);
Vec2f midb = end.midPoint(mid);
Vec2f nora = mid.cpy().sub(start).nor();
Vec2f norb = mid.cpy().sub(end).nor();
circleCenter = intersect(mida, nora, midb, norb);
Vec2f startAngPoint = start.cpy().sub(circleCenter);
Vec2f midAngPoint = mid.cpy().sub(circleCenter);
Vec2f endAngPoint = end.cpy().sub(circleCenter);
startAng = (float) Math.atan2(startAngPoint.y, startAngPoint.x);
midAng = (float) Math.atan2(midAngPoint.y, midAngPoint.x);
endAng = (float) Math.atan2(endAngPoint.y, endAngPoint.x);
//find angles that passes thru midAng
if(!isIn(startAng,midAng,endAng)){
if(Math.abs(startAng+twopi-endAng)<twopi && isIn(startAng+(twopi),midAng,endAng)){
startAng+=twopi;
}else if(Math.abs(startAng-(endAng+twopi))<twopi && isIn(startAng,midAng,endAng+(twopi))){
endAng+=twopi;
}else if(Math.abs(startAng-twopi-endAng)<twopi && isIn(startAng-(twopi),midAng,endAng)){
startAng-=twopi;
}else if(Math.abs(startAng-(endAng-twopi))<twopi && isIn(startAng,midAng,endAng-(twopi))){
endAng-=twopi;
}else{
throw new Error("Cannot find Angles between midAng "+startAng+" "+midAng+" "+endAng);
}
}
radius = startAngPoint.len();
float pixelLength = hitObject.getPixelLength() * OsuHitObject.getMultiplier();
float arcAng = pixelLength / radius; //len = theta * r / theta = len/r
//float orgArcLen = (startAng-endAng)*radius;
//System.out.println("ArgLen:"+pixelLength+" "+orgArcLen);
if(endAng>startAng){
endAng=startAng+arcAng;
}else{
endAng=startAng-arcAng;
}
drawEndAngle = (float) ((endAng+(startAng>endAng?halfpi:-halfpi)) * 180 / Math.PI);
drawStartAngle = (float) ((startAng+(startAng>endAng?-halfpi:halfpi)) * 180 / Math.PI);
}
private boolean isIn(float a,float b,float c){
return (b>a && b<c) || (b<a && b>c);
}
//http://gamedev.stackexchange.com/questions/44720/line-intersection-from-parametric-equation
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){
throw new Error("parallel ");
}
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);
}
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)};
}
public void draw() {
Image hitCircle = GameImage.HITCIRCLE.getImage();
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
//Utils.drawCentered(hitCircleOverlay, start.x, start.y, Utils.COLOR_WHITE_FADE);
//Utils.drawCentered(hitCircleOverlay, mid.x, mid.y, Utils.COLOR_WHITE_FADE);
//Utils.drawCentered(hitCircleOverlay, end.x, end.y, Utils.COLOR_WHITE_FADE);
//Utils.drawCentered(hitCircleOverlay, circleCenter.x, circleCenter.y, Utils.COLOR_WHITE_FADE);
// draw overlay and hit circle
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;
}
}
/**
* Representation of a Bezier curve, the main component of a slider.
*
@ -224,7 +395,219 @@ public class Slider implements HitObject {
Utils.drawCentered(hitCircle, curveX[i], curveY[i], color);
}
}
//Linear(ish) Bezier curve
//http://pomax.github.io/bezierinfo/#tracing
private class LinearBezier extends Curve{
/** The angles of the first and last control points. */
private float startAngle, endAngle;
LinkedList<Bezier2> beziers = new LinkedList<Bezier2>();
Vec2f[] curve;
int ncurve;
public LinearBezier(){
//splits points into different beziers if has the same points(Red points)
int npoints = hitObject.getSliderX().length + 1;
LinkedList<Vec2f> points = new LinkedList<Vec2f>();
Vec2f lastPoi = null;
for(int i=0; i<npoints; i++){
Vec2f tpoi = new Vec2f(getX(i), getY(i));
if(lastPoi!=null && tpoi.equals(lastPoi)){
beziers.add(new Bezier2(points.toArray(new Vec2f[0])));
points.clear();
}
points.add(tpoi);
lastPoi = tpoi;
}
if(points.size()<2){
throw new Error("trying to continue Beziers with less than 2 points");
}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 equal distance to eachother
ncurve = (int) (hitObject.getPixelLength()/5f);
curve = new Vec2f[ncurve+1];
float distanceAt = 0;
Iterator<Bezier2> ita = beziers.iterator();
int curPoint=0;
Bezier2 curBezier=ita.next();
Vec2f lastCurve = curBezier.curve[0];
float lastDistanceAt = 0;
//length of Bezier should equal pixel length (in 640x480)
float pixelLength = hitObject.getPixelLength()*OsuHitObject.getMultiplier();
for(int i=0;i<ncurve+1;i++){
int prefDistance = (int) (i*pixelLength/ncurve);
while(distanceAt<prefDistance){
lastDistanceAt = distanceAt;
lastCurve = curBezier.curve[curPoint];
distanceAt+=curBezier.curveDis[curPoint++];
if(curPoint >= curBezier.ncurve){
if(ita.hasNext()){
curBezier = ita.next();
curPoint = 0;
}else{
curPoint = curBezier.ncurve -1;
}
}
}
Vec2f thisCurve = curBezier.curve[curPoint];
//interpolate the point between the two closest distances
if(distanceAt-lastDistanceAt > 1){
float t = (prefDistance-lastDistanceAt)/(float)(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];
Vec2f c2 = curve[1];
startAngle = (float) (Math.atan2(c2.y - c1.y, c2.x - c1.x) * 180 / Math.PI);
c1 = curve[ncurve-1];
c2 = curve[ncurve-2];
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()+" "+hitObject.xMultiplier);
}
@Override
public float[] pointAt(float t) {
float index = t * ncurve;
if((int)index>=ncurve){
Vec2f poi = curve[ncurve-1];
return new float[]{poi.x, poi.y};
}
Vec2f poi = curve[(int)index];
float t2 = index - (int)index;
Vec2f poi2 = curve[(int)index+1];
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();
// draw overlay and hit circle
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;
}
}
private class Bezier2{
Vec2f[] points;
Vec2f[] curve;
float[] curveDis;
int ncurve;
float totalDistance;
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
ncurve= (int)(approxlength/4);
curve = new Vec2f[ncurve];
for(int i=0; i<ncurve; i++){
curve[i] = pointAt(i/(float)ncurve);
}
//find the distance of each subdivision
curveDis= new float[ncurve];
for(int i=0; i<ncurve; i++){
if(i==0)
curveDis[i] = 0;
else
curveDis[i] = curve[i].cpy().sub(curve[i-1]).len();
totalDistance+=curveDis[i];
}
//System.out.println("New Bezier2 "+points.length+" "+approxlength+" "+totalDistance());
}
public float totalDistance(){
return totalDistance;
}
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;
}
/**
* Calculates the factorial of a number.
*/
private 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 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);
}
}
private float lerp(float a, float b, float t){
return a*(1-t) + b*t;
}
//http://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
private 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);
}
/**
* Returns the x coordinate of the control point at index i.
*/
private float getX(int i) {
return (i == 0) ? hitObject.getX() : hitObject.getSliderX()[i - 1];
}
/**
* Returns the y coordinate of the control point at index i.
*/
private float getY(int i) {
return (i == 0) ? hitObject.getY() : hitObject.getSliderY()[i - 1];
}
/**
* Initializes the Slider data type with images and dimensions.
* @param container the game container
@ -268,8 +651,12 @@ public class Slider implements HitObject {
this.data = data;
this.color = color;
this.comboEnd = comboEnd;
if(hitObject.getSliderType() == 'P' && hitObject.getSliderX().length==2){
this.bezier = new CircumscribedCircle();
}else {
this.bezier = new LinearBezier();
}
this.bezier = new Bezier();
}
@Override
@ -301,10 +688,11 @@ public class Slider implements HitObject {
Image hitCircle = GameImage.HITCIRCLE.getImage();
// end circle
int lastIndex = sliderX.length - 1;
Utils.drawCentered(hitCircleOverlay, sliderX[lastIndex], sliderY[lastIndex], Utils.COLOR_WHITE_FADE);
Utils.drawCentered(hitCircle, sliderX[lastIndex], sliderY[lastIndex], color);
//int lastIndex = sliderX.length - 1;
float[] endPos = bezier.pointAt(1);
Utils.drawCentered(hitCircle, endPos[0], endPos[1], color);
Utils.drawCentered(hitCircleOverlay, endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
// start circle
Utils.drawCentered(hitCircleOverlay, x, y, Utils.COLOR_WHITE_FADE);
Utils.drawCentered(hitCircle, x, y, color);
@ -318,17 +706,23 @@ public class Slider implements HitObject {
Utils.COLOR_WHITE_FADE.a = oldAlphaFade;
// repeats
if (hitObject.getRepeatCount() - 1 > currentRepeats) {
Image arrow = GameImage.REVERSEARROW.getImage();
arrow.setAlpha(alpha);
if (currentRepeats % 2 == 0) { // last circle
arrow.setRotation(bezier.getEndAngle());
arrow.drawCentered(sliderX[lastIndex], sliderY[lastIndex]);
} else { // first circle
arrow.setRotation(bezier.getStartAngle());
arrow.drawCentered(x, y);
for(int tcurRepeat = currentRepeats; tcurRepeat<=currentRepeats+1; tcurRepeat++){
if (hitObject.getRepeatCount() - 1 > tcurRepeat) {
Image arrow = GameImage.REVERSEARROW.getImage();
if(tcurRepeat != currentRepeats){
float t = getT(trackPosition, true);
arrow.setAlpha((float) (t-Math.floor(t)));
}else{
arrow.setAlpha(1f);
}
if (tcurRepeat % 2 == 0) { // last circle
arrow.setRotation(bezier.getEndAngle());
arrow.drawCentered(endPos[0], endPos[1]);
} else { // first circle
arrow.setRotation(bezier.getStartAngle());
arrow.drawCentered(x, y);
}
}
arrow.setAlpha(1f);
}
if (timeDiff >= 0) {
@ -366,11 +760,12 @@ public class Slider implements HitObject {
else
result = GameData.HIT_MISS;
if (currentRepeats % 2 == 0) // last circle
if (currentRepeats % 2 == 0) {// last circle
float[] lastPos = bezier.pointAt(1);
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
hitObject.getSliderX()[lastIndex], hitObject.getSliderY()[lastIndex],
lastPos[0],lastPos[1],
color, comboEnd, hitObject.getHitSoundType());
else // first circle
}else // first circle
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
hitObject.getX(), hitObject.getY(), color, comboEnd, hitObject.getHitSoundType());