/*
* 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 .
*/
package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.awt.Point;
import java.util.LinkedList;
import org.newdawn.slick.*;
import yugecin.opsudance.Dancer;
import yugecin.opsudance.skinning.SkinService;
import static yugecin.opsudance.options.Options.*;
import static yugecin.opsudance.core.InstanceContainer.*;
/**
* Updates and draws the cursor.
*/
public class Cursor {
/** Last cursor coordinates. */
private Point lastPosition;
/** Cursor rotation angle. */
private static float cursorAngle = 0f;
/** The time in milliseconds when the cursor was last pressed, used for the scaling animation. */
private long lastCursorPressTime = 0L;
/** Whether or not the cursor was pressed in the last frame, used for the scaling animation. */
private boolean lastCursorPressState = false;
/** The amount the cursor scale increases, if enabled, when pressed. */
private static final float CURSOR_SCALE_CHANGE = 0.25f;
/** The time it takes for the cursor to scale, in milliseconds. */
private static final float CURSOR_SCALE_TIME = 125;
/** Stores all previous cursor locations to display a trail. */
private LinkedList trail = new LinkedList<>();
private boolean newStyle;
public static Color lastObjColor = Color.white;
public static Color lastMirroredObjColor = Color.white;
public static Color nextObjColor = Color.white;
public static Color nextMirroredObjColor = Color.white;
public static Color lastCursorColor = Color.white;
private boolean isMirrored;
public Cursor() {
this(false);
}
public Cursor(boolean isMirrored) {
resetLocations(0, 0);
this.isMirrored = isMirrored;
}
/**
* Draws the cursor.
* @param mousePressed whether or not the mouse button is pressed
*/
public void draw(boolean mousePressed) {
if (OPTION_DISABLE_CURSOR.state) {
return;
}
// determine correct cursor image
Image cursor, cursorMiddle = null, cursorTrail;
boolean beatmapSkinned = GameImage.CURSOR.hasBeatmapSkinImage();
boolean hasMiddle;
if (beatmapSkinned) {
newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
hasMiddle = GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage();
} else {
newStyle = hasMiddle = OPTION_NEW_CURSOR.state;
}
if (beatmapSkinned || newStyle) {
cursor = GameImage.CURSOR.getImage();
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
} else {
cursor = GameImage.CURSOR.hasGameSkinImage() ? GameImage.CURSOR.getImage() : GameImage.CURSOR_OLD.getImage();
cursorTrail = GameImage.CURSOR_TRAIL.hasGameSkinImage() ? GameImage.CURSOR_TRAIL.getImage() : GameImage.CURSOR_TRAIL_OLD.getImage();
}
if (hasMiddle)
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
// scale cursor
float cursorScaleAnimated = 1f;
if (SkinService.skin.isCursorExpanded()) {
if (lastCursorPressState != mousePressed) {
lastCursorPressState = mousePressed;
lastCursorPressTime = System.currentTimeMillis();
}
float cursorScaleChange = CURSOR_SCALE_CHANGE * AnimationEquation.IN_OUT_CUBIC.calc(
Utils.clamp(System.currentTimeMillis() - lastCursorPressTime, 0, CURSOR_SCALE_TIME) / CURSOR_SCALE_TIME);
cursorScaleAnimated = 1f + ((mousePressed) ? cursorScaleChange : CURSOR_SCALE_CHANGE - cursorScaleChange);
}
float cursorScale = cursorScaleAnimated * OPTION_CURSOR_SIZE.val / 100f;
if (cursorScale != 1f) {
cursor = cursor.getScaledCopy(cursorScale);
cursorTrail = cursorTrail.getScaledCopy(cursorScale);
}
Color filter;
if (isMirrored) {
filter = Dancer.cursorColorMirrorOverride.getMirrorColor();
} else {
lastCursorColor = filter = Dancer.cursorColorOverride.getColor();
}
// draw a fading trail
float alpha = 0f;
float t = 2f / trail.size();
int cursorTrailWidth = cursorTrail.getWidth(), cursorTrailHeight = cursorTrail.getHeight();
float cursorTrailRotation = (SkinService.skin.isCursorTrailRotated()) ? cursorAngle : 0;
cursorTrail.startUse();
for (Point p : trail) {
alpha += t;
cursorTrail.setImageColor(filter.r, filter.g, filter.b, alpha);
cursorTrail.drawEmbedded(
p.x - (cursorTrailWidth / 2f), p.y - (cursorTrailHeight / 2f),
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
}
cursorTrail.drawEmbedded(
lastPosition.x - (cursorTrailWidth / 2f), lastPosition.y - (cursorTrailHeight / 2f),
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
cursorTrail.endUse();
// draw the other components
if (newStyle && SkinService.skin.isCursorRotated()) {
cursor.setRotation(cursorAngle);
}
cursor.drawCentered(lastPosition.x, lastPosition.y, OPTION_DANCE_CURSOR_ONLY_COLOR_TRAIL.state ? Color.white : filter);
if (hasMiddle) {
cursorMiddle.drawCentered(lastPosition.x, lastPosition.y, OPTION_DANCE_CURSOR_ONLY_COLOR_TRAIL.state ? Color.white : filter);
}
}
/**
* Sets the cursor position to given point and updates trail.
* @param mouseX x coordinate to set position to
* @param mouseY y coordinate to set position to
*/
public void setCursorPosition(int delta, int mouseX, int mouseY) {
// TODO: use an image buffer
int removeCount = 0;
float FPSmod = Math.max(1000 / Math.max(delta, 1), 1) / 30f; // TODO
if (newStyle) {
// new style: add all points between cursor movements
if ((lastPosition.x == 0 && lastPosition.y == 0) || !addCursorPoints(lastPosition.x, lastPosition.y, mouseX, mouseY)) {
trail.add(new Point(mouseX, mouseY));
}
lastPosition.move(mouseX, mouseY);
removeCount = (int) (trail.size() / (6 * FPSmod)) + 1;
} else {
// old style: sample one point at a time
trail.add(new Point(mouseX, mouseY));
int max = (int) (10 * FPSmod);
if (trail.size() > max)
removeCount = trail.size() - max;
}
int cursortraillength = OPTION_DANCE_CURSOR_TRAIL_OVERRIDE.val;
if (cursortraillength > 20) {
removeCount = trail.size() - cursortraillength;
}
// remove points from the lists
for (int i = 0; i < removeCount && !trail.isEmpty(); i++)
trail.remove();
}
/**
* Adds all points between (x1, y1) and (x2, y2) to the cursor point lists.
* @author http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm#Java
*/
private boolean addCursorPoints(int x1, int y1, int x2, int y2) {
// delta of exact value and rounded value of the dependent variable
boolean added = false;
int d = 0;
int dy = Math.abs(y2 - y1);
int dx = Math.abs(x2 - x1);
int dy2 = (dy << 1); // slope scaling factors to avoid floating
int dx2 = (dx << 1); // point
int ix = x1 < x2 ? 1 : -1; // increment direction
int iy = y1 < y2 ? 1 : -1;
int k = 5; // sample size
if (dy <= dx) {
for (int i = 0; ; i++) {
if (i == k) {
trail.add(new Point(x1, y1));
added = true;
i = 0;
}
if (x1 == x2)
break;
x1 += ix;
d += dy2;
if (d > dx) {
y1 += iy;
d -= dx2;
}
}
} else {
for (int i = 0; ; i++) {
if (i == k) {
trail.add(new Point(x1, y1));
added = true;
i = 0;
}
if (y1 == y2)
break;
y1 += iy;
d += dx2;
if (d > dy) {
x1 += ix;
d -= dy2;
}
}
}
return added;
}
/**
* Rotates the cursor by a degree determined by a delta interval.
* If the old style cursor is being used, this will do nothing.
* @param delta the delta interval since the last call
*/
public void updateAngle() {
cursorAngle += renderDelta / 40f;
cursorAngle %= 360;
}
/**
* Resets all cursor data and beatmap skins.
*/
public void reset(int mouseX, int mouseY) {
// destroy skin images
GameImage.CURSOR.destroyBeatmapSkinImage();
GameImage.CURSOR_MIDDLE.destroyBeatmapSkinImage();
GameImage.CURSOR_TRAIL.destroyBeatmapSkinImage();
// reset locations
resetLocations(mouseX, mouseY);
// reset angles
cursorAngle = 0f;
}
/**
* Resets all cursor location data.
*/
public void resetLocations(int mouseX, int mouseY) {
trail.clear();
lastPosition = new Point(mouseX, mouseY);
for (int i = 0; i < 50; i++) {
trail.add(new Point(lastPosition));
}
}
/**
* Returns whether or not the cursor is skinned.
*/
public boolean isBeatmapSkinned() {
return (GameImage.CURSOR.hasBeatmapSkinImage() ||
GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage() ||
GameImage.CURSOR_TRAIL.hasBeatmapSkinImage());
}
}