Merge remote-tracking branch 'org/master' into KinecticScrolling

Conflicts:
	src/itdelatrisu/opsu/ScoreData.java
	src/itdelatrisu/opsu/downloads/DownloadNode.java
	src/itdelatrisu/opsu/states/DownloadsMenu.java
	src/itdelatrisu/opsu/states/SongMenu.java
This commit is contained in:
fd
2015-09-14 19:32:18 -04:00
102 changed files with 6168 additions and 1857 deletions

View File

@@ -0,0 +1,51 @@
/*
* 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.ui;
import org.newdawn.slick.Color;
/**
* Colors used for drawing.
*/
public class Colors {
public static final Color
BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
WHITE_ALPHA = new Color(255, 255, 255, 0.5f),
BLUE_DIVIDER = new Color(49, 94, 237),
BLUE_BACKGROUND = new Color(74, 130, 255),
BLUE_BUTTON = new Color(40, 129, 237),
ORANGE_BUTTON = new Color(200, 90, 3),
YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
WHITE_FADE = new Color(255, 255, 255, 1f),
RED_HOVER = new Color(255, 112, 112),
GREEN = new Color(137, 201, 79),
LIGHT_ORANGE = new Color(255, 192, 128),
LIGHT_GREEN = new Color(128, 255, 128),
LIGHT_BLUE = new Color(128, 128, 255),
GREEN_SEARCH = new Color(173, 255, 47),
DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
RED_HIGHLIGHT = new Color(246, 154, 161),
BLUE_HIGHLIGHT = new Color(173, 216, 230),
BLACK_BG_NORMAL = new Color(0, 0, 0, 0.25f),
BLACK_BG_HOVER = new Color(0, 0, 0, 0.5f),
BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f);
// This class should not be instantiated.
private Colors() {}
}

View File

@@ -24,9 +24,10 @@ import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.skins.Skin;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.awt.Point;
import java.nio.IntBuffer;
import java.util.Iterator;
import java.util.LinkedList;
import org.lwjgl.BufferUtils;
@@ -45,13 +46,25 @@ public class Cursor {
private static org.lwjgl.input.Cursor emptyCursor;
/** Last cursor coordinates. */
private int lastX = -1, lastY = -1;
private Point lastPosition;
/** Cursor rotation angle. */
private 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<Integer> cursorX, cursorY;
private LinkedList<Point> trail = new LinkedList<Point>();
// game-related variables
private static GameContainer container;
@@ -81,10 +94,7 @@ public class Cursor {
/**
* Constructor.
*/
public Cursor() {
cursorX = new LinkedList<Integer>();
cursorY = new LinkedList<Integer>();
}
public Cursor() {}
/**
* Draws the cursor.
@@ -105,85 +115,90 @@ public class Cursor {
* @param mousePressed whether or not the mouse button is pressed
*/
public void draw(int mouseX, int mouseY, boolean mousePressed) {
if (Options.isCursorDisabled())
return;
// determine correct cursor image
Image cursor = null, cursorMiddle = null, cursorTrail = null;
boolean skinned = GameImage.CURSOR.hasSkinImage();
boolean beatmapSkinned = GameImage.CURSOR.hasBeatmapSkinImage();
boolean newStyle, hasMiddle;
if (skinned) {
Skin skin = Options.getSkin();
if (beatmapSkinned) {
newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage();
hasMiddle = GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage();
} else
newStyle = hasMiddle = Options.isNewCursorEnabled();
if (skinned || newStyle) {
if (newStyle || beatmapSkinned) {
cursor = GameImage.CURSOR.getImage();
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
} else {
cursor = GameImage.CURSOR_OLD.getImage();
cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage();
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();
int removeCount = 0;
int FPSmod = (Options.getTargetFPS() / 60);
Skin skin = Options.getSkin();
// scale cursor
float cursorScale = Options.getCursorScale();
if (mousePressed && skin.isCursorExpanded())
cursorScale *= 1.25f; // increase the cursor size if pressed
float cursorScaleAnimated = 1f;
if (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 * Options.getCursorScale();
if (cursorScale != 1f) {
cursor = cursor.getScaledCopy(cursorScale);
cursorTrail = cursorTrail.getScaledCopy(cursorScale);
if (hasMiddle)
cursorMiddle = cursorMiddle.getScaledCopy(cursorScale);
}
// TODO: use an image buffer
int removeCount = 0;
float FPSmod = Math.max(container.getFPS(), 1) / 60f;
if (newStyle) {
// new style: add all points between cursor movements
if (lastX < 0) {
lastX = mouseX;
lastY = mouseY;
if (lastPosition == null) {
lastPosition = new Point(mouseX, mouseY);
return;
}
addCursorPoints(lastX, lastY, mouseX, mouseY);
lastX = mouseX;
lastY = mouseY;
addCursorPoints(lastPosition.x, lastPosition.y, mouseX, mouseY);
lastPosition.move(mouseX, mouseY);
removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
removeCount = (int) (trail.size() / (6 * FPSmod)) + 1;
} else {
// old style: sample one point at a time
cursorX.add(mouseX);
cursorY.add(mouseY);
trail.add(new Point(mouseX, mouseY));
int max = 10 * FPSmod;
if (cursorX.size() > max)
removeCount = cursorX.size() - max;
int max = (int) (10 * FPSmod);
if (trail.size() > max)
removeCount = trail.size() - max;
}
// remove points from the lists
for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
cursorX.remove();
cursorY.remove();
}
for (int i = 0; i < removeCount && !trail.isEmpty(); i++)
trail.remove();
// draw a fading trail
float alpha = 0f;
float t = 2f / cursorX.size();
if (skin.isCursorTrailRotated())
cursorTrail.setRotation(cursorAngle);
Iterator<Integer> iterX = cursorX.iterator();
Iterator<Integer> iterY = cursorY.iterator();
while (iterX.hasNext()) {
int cx = iterX.next();
int cy = iterY.next();
float t = 2f / trail.size();
int cursorTrailWidth = cursorTrail.getWidth(), cursorTrailHeight = cursorTrail.getHeight();
float cursorTrailRotation = (skin.isCursorTrailRotated()) ? cursorAngle : 0;
cursorTrail.startUse();
for (Point p : trail) {
alpha += t;
cursorTrail.setAlpha(alpha);
// if (cx != x || cy != y)
cursorTrail.drawCentered(cx, cy);
cursorTrail.setImageColor(1f, 1f, 1f, alpha);
cursorTrail.drawEmbedded(
p.x - (cursorTrailWidth / 2f), p.y - (cursorTrailHeight / 2f),
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
}
cursorTrail.drawCentered(mouseX, mouseY);
cursorTrail.drawEmbedded(
mouseX - (cursorTrailWidth / 2f), mouseY - (cursorTrailHeight / 2f),
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
cursorTrail.endUse();
// draw the other components
if (newStyle && skin.isCursorRotated())
@@ -212,8 +227,7 @@ public class Cursor {
if (dy <= dx) {
for (int i = 0; ; i++) {
if (i == k) {
cursorX.add(x1);
cursorY.add(y1);
trail.add(new Point(x1, y1));
i = 0;
}
if (x1 == x2)
@@ -228,8 +242,7 @@ public class Cursor {
} else {
for (int i = 0; ; i++) {
if (i == k) {
cursorX.add(x1);
cursorY.add(y1);
trail.add(new Point(x1, y1));
i = 0;
}
if (y1 == y2)
@@ -255,13 +268,13 @@ public class Cursor {
}
/**
* Resets all cursor data and skins.
* Resets all cursor data and beatmap skins.
*/
public void reset() {
// destroy skin images
GameImage.CURSOR.destroySkinImage();
GameImage.CURSOR_MIDDLE.destroySkinImage();
GameImage.CURSOR_TRAIL.destroySkinImage();
GameImage.CURSOR.destroyBeatmapSkinImage();
GameImage.CURSOR_MIDDLE.destroyBeatmapSkinImage();
GameImage.CURSOR_TRAIL.destroyBeatmapSkinImage();
// reset locations
resetLocations();
@@ -276,18 +289,17 @@ public class Cursor {
* Resets all cursor location data.
*/
public void resetLocations() {
lastX = lastY = -1;
cursorX.clear();
cursorY.clear();
lastPosition = null;
trail.clear();
}
/**
* Returns whether or not the cursor is skinned.
*/
public boolean isSkinned() {
return (GameImage.CURSOR.hasSkinImage() ||
GameImage.CURSOR_MIDDLE.hasSkinImage() ||
GameImage.CURSOR_TRAIL.hasSkinImage());
public boolean isBeatmapSkinned() {
return (GameImage.CURSOR.hasBeatmapSkinImage() ||
GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage() ||
GameImage.CURSOR_TRAIL.hasBeatmapSkinImage());
}
/**

View File

@@ -0,0 +1,424 @@
/*
* 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.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Color;
import org.newdawn.slick.Font;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.gui.AbstractComponent;
import org.newdawn.slick.gui.GUIContext;
/**
* Simple dropdown menu.
* <p>
* Basic usage:
* <ul>
* <li>Override {@link #menuClicked(int)} to perform actions when the menu is clicked
* (e.g. play a sound effect, block input under certain conditions).
* <li>Override {@link #itemSelected(int, Object)} to perform actions when a new item is selected.
* <li>Call {@link #activate()}/{@link #deactivate()} whenever the component is needed
* (e.g. in a state's {@code enter} and {@code leave} events.
* </ul>
*
* @param <E> the type of the elements in the menu
*/
public class DropdownMenu<E> extends AbstractComponent {
/** Padding ratios for drawing. */
private static final float PADDING_Y = 0.1f, CHEVRON_X = 0.03f;
/** Whether this component is active. */
private boolean active;
/** The menu items. */
private E[] items;
/** The menu item names. */
private String[] itemNames;
/** The index of the selected item. */
private int itemIndex = 0;
/** Whether the menu is expanded. */
private boolean expanded = false;
/** The expanding animation progress. */
private AnimatedValue expandProgress = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR);
/** The last update time, in milliseconds. */
private long lastUpdateTime;
/** The top-left coordinates. */
private float x, y;
/** The width and height of the dropdown menu. */
private int width, height;
/** The height of the base item. */
private int baseHeight;
/** The vertical offset between items. */
private float offsetY;
/** The colors to use. */
private Color
textColor = Color.white, backgroundColor = Color.black,
highlightColor = Colors.BLUE_DIVIDER, borderColor = Colors.BLUE_DIVIDER,
chevronDownColor = textColor, chevronRightColor = backgroundColor;
/** The fonts to use. */
private UnicodeFont fontNormal = Fonts.MEDIUM, fontSelected = Fonts.MEDIUMBOLD;
/** The chevron images. */
private Image chevronDown, chevronRight;
/**
* Creates a new dropdown menu.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y) {
this(container, items, x, y, 0);
}
/**
* Creates a new dropdown menu with the given fonts.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param normal the normal font
* @param selected the font for the selected item
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, UnicodeFont normal, UnicodeFont selected) {
this(container, items, x, y, 0, normal, selected);
}
/**
* Creates a new dropdown menu with the given width.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param width the menu width
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, int width) {
super(container);
init(items, x, y, width);
}
/**
* Creates a new dropdown menu with the given width and fonts.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param width the menu width
* @param normal the normal font
* @param selected the font for the selected item
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, int width, UnicodeFont normal, UnicodeFont selected) {
super(container);
this.fontNormal = normal;
this.fontSelected = selected;
init(items, x, y, width);
}
/**
* Returns the maximum item width from the list.
*/
private int getMaxItemWidth() {
int maxWidth = 0;
for (int i = 0; i < itemNames.length; i++) {
int w = fontSelected.getWidth(itemNames[i]);
if (w > maxWidth)
maxWidth = w;
}
return maxWidth;
}
/**
* Initializes the component.
*/
private void init(E[] items, float x, float y, int width) {
this.items = items;
this.itemNames = new String[items.length];
for (int i = 0; i < itemNames.length; i++)
itemNames[i] = items[i].toString();
this.x = x;
this.y = y;
this.baseHeight = fontNormal.getLineHeight();
this.offsetY = baseHeight + baseHeight * PADDING_Y;
this.height = (int) (offsetY * (items.length + 1));
int chevronDownSize = baseHeight * 4 / 5;
this.chevronDown = GameImage.CHEVRON_DOWN.getImage().getScaledCopy(chevronDownSize, chevronDownSize);
int chevronRightSize = baseHeight * 2 / 3;
this.chevronRight = GameImage.CHEVRON_RIGHT.getImage().getScaledCopy(chevronRightSize, chevronRightSize);
int maxItemWidth = getMaxItemWidth();
int minWidth = maxItemWidth + chevronRight.getWidth() * 2;
this.width = Math.max(width, minWidth);
}
@Override
public void setLocation(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int getX() { return (int) x; }
@Override
public int getY() { return (int) y; }
@Override
public int getWidth() { return width; }
@Override
public int getHeight() { return (expanded) ? height : baseHeight; }
/** Activates the component. */
public void activate() { this.active = true; }
/** Deactivates the component. */
public void deactivate() { this.active = false; }
/**
* Returns whether the dropdown menu is currently open.
* @return true if open, false otherwise
*/
public boolean isOpen() { return expanded; }
/**
* Opens or closes the dropdown menu.
* @param flag true to open, false to close
*/
public void open(boolean flag) { this.expanded = flag; }
/**
* Returns true if the coordinates are within the menu bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public boolean contains(float cx, float cy) {
return (cx > x && cx < x + width && (
(cy > y && cy < y + baseHeight) ||
(expanded && cy > y + offsetY && cy < y + height)));
}
/**
* Returns true if the coordinates are within the base item bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public boolean baseContains(float cx, float cy) {
return (cx > x && cx < x + width && cy > y && cy < y + baseHeight);
}
@Override
public void render(GUIContext container, Graphics g) throws SlickException {
// update animation
long time = container.getTime();
if (lastUpdateTime > 0) {
int delta = (int) (time - lastUpdateTime);
expandProgress.update((expanded) ? delta : -delta * 2);
}
this.lastUpdateTime = time;
// get parameters
Input input = container.getInput();
int idx = getIndexAt(input.getMouseX(), input.getMouseY());
float t = expandProgress.getValue();
if (expanded)
t = AnimationEquation.OUT_CUBIC.calc(t);
// background and border
Color oldGColor = g.getColor();
float oldLineWidth = g.getLineWidth();
final int cornerRadius = 6;
g.setLineWidth(1f);
g.setColor((idx == -1) ? highlightColor : backgroundColor);
g.fillRoundRect(x, y, width, baseHeight, cornerRadius);
g.setColor(borderColor);
g.drawRoundRect(x, y, width, baseHeight, cornerRadius);
if (expanded || t >= 0.0001) {
float oldBackgroundAlpha = backgroundColor.a;
backgroundColor.a *= t;
g.setColor(backgroundColor);
g.fillRoundRect(x, y + offsetY, width, (height - offsetY) * t, cornerRadius);
backgroundColor.a = oldBackgroundAlpha;
}
if (idx >= 0 && t >= 0.9999) {
g.setColor(highlightColor);
float yPos = y + offsetY + (offsetY * idx);
int yOff = 0, hOff = 0;
if (idx == 0 || idx == items.length - 1) {
g.fillRoundRect(x, yPos, width, offsetY, cornerRadius);
if (idx == 0)
yOff = cornerRadius;
hOff = cornerRadius;
}
g.fillRect(x, yPos + yOff, width, offsetY - hOff);
}
g.setColor(oldGColor);
g.setLineWidth(oldLineWidth);
// text
chevronDown.draw(x + width - chevronDown.getWidth() - width * CHEVRON_X, y + (baseHeight - chevronDown.getHeight()) / 2f, chevronDownColor);
fontNormal.drawString(x + (width * 0.03f), y + (fontNormal.getPaddingTop() + fontNormal.getPaddingBottom()) / 2f, itemNames[itemIndex], textColor);
float oldTextAlpha = textColor.a;
textColor.a *= t;
if (expanded || t >= 0.0001) {
for (int i = 0; i < itemNames.length; i++) {
Font f = (i == itemIndex) ? fontSelected : fontNormal;
if (i == idx && t >= 0.999)
chevronRight.draw(x, y + offsetY + (offsetY * i) + (offsetY - chevronRight.getHeight()) / 2f, chevronRightColor);
f.drawString(x + chevronRight.getWidth(), y + offsetY + (offsetY * i * t), itemNames[i], textColor);
}
}
textColor.a = oldTextAlpha;
}
/**
* Returns the index of the item at the given location, -1 for the base item,
* and -2 if there is no item at the location.
* @param cx the x coordinate
* @param cy the y coordinate
*/
private int getIndexAt(float cx, float cy) {
if (!contains(cx, cy))
return -2;
if (cy <= y + baseHeight)
return -1;
if (!expanded)
return -2;
return (int) ((cy - (y + offsetY)) / offsetY);
}
/**
* Resets the menu state.
*/
public void reset() {
this.expanded = false;
this.lastUpdateTime = 0;
expandProgress.setTime(0);
}
@Override
public void mousePressed(int button, int x, int y) {
if (!active)
return;
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
int idx = getIndexAt(x, y);
if (idx == -2) {
this.expanded = false;
return;
}
if (!menuClicked(idx))
return;
this.expanded = (idx == -1) ? !expanded : false;
if (idx >= 0 && itemIndex != idx) {
this.itemIndex = idx;
itemSelected(idx, items[idx]);
}
consumeEvent();
}
/**
* Notification that a new item was selected (via override).
* @param index the index of the item selected
* @param item the item selected
*/
public void itemSelected(int index, E item) {}
/**
* Notification that the menu was clicked (via override).
* @param index the index of the item clicked, or -1 for the base item
* @return true to process the click, or false to block/intercept it
*/
public boolean menuClicked(int index) { return true; }
@Override
public void setFocus(boolean focus) { /* does not currently use the "focus" concept */ }
@Override
public void mouseReleased(int button, int x, int y) { /* does not currently use the "focus" concept */ }
/**
* Selects the item at the given index.
* @param index the list item index
* @throws IllegalArgumentException if {@code index} is negative or greater than or equal to size
*/
public void setSelectedIndex(int index) {
if (index < 0 || index >= items.length)
throw new IllegalArgumentException();
this.itemIndex = index;
}
/**
* Returns the index of the selected item.
*/
public int getSelectedIndex() { return itemIndex; }
/**
* Returns the selected item.
*/
public E getSelectedItem() { return items[itemIndex]; }
/**
* Returns the item at the given index.
* @param index the list item index
*/
public E getItemAt(int index) { return items[index]; }
/**
* Returns the number of items in the list.
*/
public int getItemCount() { return items.length; }
/** Sets the text color. */
public void setTextColor(Color c) { this.textColor = c; }
/** Sets the background color. */
public void setBackgroundColor(Color c) { this.backgroundColor = c; }
/** Sets the highlight color. */
public void setHighlightColor(Color c) { this.highlightColor = c; }
/** Sets the border color. */
public void setBorderColor(Color c) { this.borderColor = c; }
/** Sets the down chevron color. */
public void setChevronDownColor(Color c) { this.chevronDownColor = c; }
/** Sets the right chevron color. */
public void setChevronRightColor(Color c) { this.chevronRightColor = c; }
}

View File

@@ -0,0 +1,156 @@
/*
* 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.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.effects.ColorEffect;
import org.newdawn.slick.font.effects.Effect;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/**
* Fonts used for drawing.
*/
public class Fonts {
public static UnicodeFont DEFAULT, BOLD, XLARGE, LARGE, MEDIUM, MEDIUMBOLD, SMALL;
/** Set of all Unicode strings already loaded per font. */
private static HashMap<UnicodeFont, HashSet<String>> loadedGlyphs = new HashMap<UnicodeFont, HashSet<String>>();
// This class should not be instantiated.
private Fonts() {}
/**
* Initializes all fonts.
* @throws SlickException if ASCII glyphs could not be loaded
* @throws FontFormatException if any font stream data does not contain the required font tables
* @throws IOException if a font stream cannot be completely read
*/
public static void init() throws SlickException, FontFormatException, IOException {
float fontBase = 12f * GameImage.getUIscale();
Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3));
DEFAULT = new UnicodeFont(font);
BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
XLARGE = new UnicodeFont(font.deriveFont(fontBase * 3));
LARGE = new UnicodeFont(font.deriveFont(fontBase * 2));
MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
MEDIUMBOLD = new UnicodeFont(font.deriveFont(Font.BOLD, fontBase * 3 / 2));
SMALL = new UnicodeFont(font.deriveFont(fontBase));
ColorEffect colorEffect = new ColorEffect();
loadFont(DEFAULT, colorEffect);
loadFont(BOLD, colorEffect);
loadFont(XLARGE, colorEffect);
loadFont(LARGE, colorEffect);
loadFont(MEDIUM, colorEffect);
loadFont(MEDIUMBOLD, colorEffect);
loadFont(SMALL, colorEffect);
}
/**
* Loads a Unicode font and its ASCII glyphs.
* @param font the font to load
* @param effect the font effect
* @throws SlickException if the glyphs could not be loaded
*/
@SuppressWarnings("unchecked")
private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
font.addAsciiGlyphs();
font.getEffects().add(effect);
font.loadGlyphs();
}
/**
* Adds and loads glyphs for a font.
* @param font the font to add the glyphs to
* @param s the string containing the glyphs to load
*/
public static void loadGlyphs(UnicodeFont font, String s) {
if (s == null || s.isEmpty())
return;
// get set of added strings
HashSet<String> set = loadedGlyphs.get(font);
if (set == null) {
set = new HashSet<String>();
loadedGlyphs.put(font, set);
} else if (set.contains(s))
return; // string already in set
// load glyphs
font.addGlyphs(s);
set.add(s);
try {
font.loadGlyphs();
} catch (SlickException e) {
Log.warn(String.format("Failed to load glyphs for string '%s'.", s), e);
}
}
/**
* Wraps the given string into a list of split lines based on the width.
* @param font the font used to draw the string
* @param text the text to split
* @param width the maximum width of a line
* @return the list of split strings
* @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3778)
*/
public static List<String> wrap(org.newdawn.slick.Font font, String text, int width) {
List<String> list = new ArrayList<String>();
String str = text;
String line = "";
int i = 0;
int lastSpace = -1;
while (i < str.length()) {
char c = str.charAt(i);
if (Character.isWhitespace(c))
lastSpace = i;
String append = line + c;
if (font.getWidth(append) > width) {
int split = (lastSpace != -1) ? lastSpace : i;
int splitTrimmed = split;
if (lastSpace != -1 && split < str.length() - 1)
splitTrimmed++;
list.add(str.substring(0, split));
str = str.substring(splitTrimmed);
line = "";
i = 0;
lastSpace = -1;
} else {
line = append;
i++;
}
}
if (str.length() != 0)
list.add(str);
return list;
}
}

View File

@@ -19,6 +19,8 @@
package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
@@ -63,11 +65,26 @@ public class MenuButton {
/** The hover actions for this button. */
private int hoverEffect = 0;
/** The current and max scale of the button. */
private float scale = 1f, hoverScale = 1.25f;
/** The hover animation duration, in milliseconds. */
private int animationDuration = 100;
/** The current and base alpha level of the button. */
private float alpha = 1f, baseAlpha = 0.75f;
/** The hover animation equation. */
private AnimationEquation animationEqn = AnimationEquation.LINEAR;
/** Whether the animation is advancing forwards (if advancing automatically). */
private boolean autoAnimationForward = true;
/** The scale of the button. */
private AnimatedValue scale;
/** The default max scale of the button. */
private static final float DEFAULT_SCALE_MAX = 1.25f;
/** The alpha level of the button. */
private AnimatedValue alpha;
/** The default base alpha level of the button. */
private static final float DEFAULT_ALPHA_BASE = 0.75f;
/** The scaled expansion direction for the button. */
private Expand dir = Expand.CENTER;
@@ -75,8 +92,11 @@ public class MenuButton {
/** Scaled expansion directions. */
public enum Expand { CENTER, UP, RIGHT, LEFT, DOWN, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; }
/** The current and max rotation angles of the button. */
private float angle = 0f, maxAngle = 30f;
/** The rotation angle of the button. */
private AnimatedValue angle;
/** The default max rotation angle of the button. */
private static final float DEFAULT_ANGLE_MAX = 30f;
/**
* Creates a new button from an Image.
@@ -126,11 +146,13 @@ public class MenuButton {
/**
* Sets a new center x coordinate.
* @param x the x coordinate
*/
public void setX(float x) { this.x = x; }
/**
* Sets a new center y coordinate.
* @param y the y coordinate
*/
public void setY(float y) { this.y = y; }
@@ -192,15 +214,15 @@ public class MenuButton {
float oldAlpha = image.getAlpha();
float oldAngle = image.getRotation();
if ((hoverEffect & EFFECT_EXPAND) > 0) {
if (scale != 1f) {
image = image.getScaledCopy(scale);
if (scale.getValue() != 1f) {
image = image.getScaledCopy(scale.getValue());
image.setAlpha(oldAlpha);
}
}
if ((hoverEffect & EFFECT_FADE) > 0)
image.setAlpha(alpha);
image.setAlpha(alpha.getValue());
if ((hoverEffect & EFFECT_ROTATE) > 0)
image.setRotation(angle);
image.setRotation(angle.getValue());
image.draw(x - xRadius, y - yRadius, filter);
if (image == this.img) {
image.setAlpha(oldAlpha);
@@ -217,9 +239,10 @@ public class MenuButton {
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
} else if ((hoverEffect & EFFECT_FADE) > 0) {
float a = image.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha();
image.setAlpha(alpha);
imgL.setAlpha(alpha);
imgR.setAlpha(alpha);
float currentAlpha = alpha.getValue();
image.setAlpha(currentAlpha);
imgL.setAlpha(currentAlpha);
imgR.setAlpha(currentAlpha);
image.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter);
imgL.draw(x - xRadius, y - yRadius, filter);
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
@@ -267,28 +290,63 @@ public class MenuButton {
*/
public void resetHover() {
if ((hoverEffect & EFFECT_EXPAND) > 0) {
this.scale = 1f;
scale.setTime(0);
setHoverRadius();
}
if ((hoverEffect & EFFECT_FADE) > 0)
this.alpha = baseAlpha;
alpha.setTime(0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
this.angle = 0f;
angle.setTime(0);
autoAnimationForward = true;
}
/**
* Removes all hover effects that have been set for the button.
*/
public void removeHoverEffects() { hoverEffect = 0; }
public void removeHoverEffects() {
this.hoverEffect = 0;
this.scale = null;
this.alpha = null;
this.angle = null;
autoAnimationForward = true;
}
/**
* Sets the hover animation duration.
* @param duration the duration, in milliseconds
*/
public void setHoverAnimationDuration(int duration) {
this.animationDuration = duration;
if (scale != null)
scale.setDuration(duration);
if (alpha != null)
alpha.setDuration(duration);
if (angle != null)
angle.setDuration(duration);
}
/**
* Sets the hover animation equation.
* @param eqn the equation to use
*/
public void setHoverAnimationEquation(AnimationEquation eqn) {
this.animationEqn = eqn;
if (scale != null)
scale.setEquation(eqn);
if (alpha != null)
alpha.setEquation(eqn);
if (angle != null)
angle.setEquation(eqn);
}
/**
* Sets the "expand" hover effect.
*/
public void setHoverExpand() { hoverEffect |= EFFECT_EXPAND; }
public void setHoverExpand() { setHoverExpand(DEFAULT_SCALE_MAX, this.dir); }
/**
* Sets the "expand" hover effect.
* @param scale the maximum scale factor (default 1.25f)
* @param scale the maximum scale factor
*/
public void setHoverExpand(float scale) { setHoverExpand(scale, this.dir); }
@@ -296,45 +354,45 @@ public class MenuButton {
* Sets the "expand" hover effect.
* @param dir the expansion direction
*/
public void setHoverExpand(Expand dir) { setHoverExpand(this.hoverScale, dir); }
public void setHoverExpand(Expand dir) { setHoverExpand(DEFAULT_SCALE_MAX, dir); }
/**
* Sets the "expand" hover effect.
* @param scale the maximum scale factor (default 1.25f)
* @param scale the maximum scale factor
* @param dir the expansion direction
*/
public void setHoverExpand(float scale, Expand dir) {
hoverEffect |= EFFECT_EXPAND;
this.hoverScale = scale;
this.scale = new AnimatedValue(animationDuration, 1f, scale, animationEqn);
this.dir = dir;
}
/**
* Sets the "fade" hover effect.
*/
public void setHoverFade() { hoverEffect |= EFFECT_FADE; }
public void setHoverFade() { setHoverFade(DEFAULT_ALPHA_BASE); }
/**
* Sets the "fade" hover effect.
* @param baseAlpha the base alpha level to fade in from (default 0.7f)
* @param baseAlpha the base alpha level to fade in from
*/
public void setHoverFade(float baseAlpha) {
hoverEffect |= EFFECT_FADE;
this.baseAlpha = baseAlpha;
this.alpha = new AnimatedValue(animationDuration, baseAlpha, 1f, animationEqn);
}
/**
* Sets the "rotate" hover effect.
*/
public void setHoverRotate() { hoverEffect |= EFFECT_ROTATE; }
public void setHoverRotate() { setHoverRotate(DEFAULT_ANGLE_MAX); }
/**
* Sets the "rotate" hover effect.
* @param maxAngle the maximum rotation angle, in degrees (default 30f)
* @param maxAngle the maximum rotation angle, in degrees
*/
public void setHoverRotate(float maxAngle) {
hoverEffect |= EFFECT_ROTATE;
this.maxAngle = maxAngle;
this.angle = new AnimatedValue(animationDuration, 0f, maxAngle, animationEqn);
}
/**
@@ -371,45 +429,53 @@ public class MenuButton {
if (hoverEffect == 0)
return;
int d = delta * (isHover ? 1 : -1);
// scale the button
if ((hoverEffect & EFFECT_EXPAND) > 0) {
int sign = 0;
if (isHover && scale < hoverScale)
sign = 1;
else if (!isHover && scale > 1f)
sign = -1;
if (sign != 0) {
scale = Utils.getBoundedValue(scale, sign * (hoverScale - 1f) * delta / 100f, 1, hoverScale);
if (scale.update(d))
setHoverRadius();
}
}
// fade the button
if ((hoverEffect & EFFECT_FADE) > 0) {
int sign = 0;
if (isHover && alpha < 1f)
sign = 1;
else if (!isHover && alpha > baseAlpha)
sign = -1;
if (sign != 0)
alpha = Utils.getBoundedValue(alpha, sign * (1f - baseAlpha) * delta / 200f, baseAlpha, 1f);
}
if ((hoverEffect & EFFECT_FADE) > 0)
alpha.update(d);
// rotate the button
if ((hoverEffect & EFFECT_ROTATE) > 0) {
int sign = 0;
boolean right = (maxAngle > 0);
if (isHover && angle != maxAngle)
sign = (right) ? 1 : -1;
else if (!isHover && angle != 0)
sign = (right) ? -1 : 1;
if (sign != 0) {
float diff = sign * Math.abs(maxAngle) * delta / 125f;
angle = (right) ?
Utils.getBoundedValue(angle, diff, 0, maxAngle) :
Utils.getBoundedValue(angle, diff, maxAngle, 0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
angle.update(d);
}
/**
* Automatically advances the hover animation in a loop.
* @param delta the delta interval
* @param reverseAtEnd whether to reverse or restart the animation upon reaching the end
*/
public void autoHoverUpdate(int delta, boolean reverseAtEnd) {
if (hoverEffect == 0)
return;
int time = ((hoverEffect & EFFECT_EXPAND) > 0) ? scale.getTime() :
((hoverEffect & EFFECT_FADE) > 0) ? alpha.getTime() :
((hoverEffect & EFFECT_ROTATE) > 0) ? angle.getTime() : -1;
if (time == -1)
return;
int d = delta * (autoAnimationForward ? 1 : -1);
if (Utils.clamp(time + d, 0, animationDuration) == time) {
if (reverseAtEnd)
autoAnimationForward = !autoAnimationForward;
else {
if ((hoverEffect & EFFECT_EXPAND) > 0)
scale.setTime(0);
if ((hoverEffect & EFFECT_FADE) > 0)
alpha.setTime(0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
angle.setTime(0);
}
}
hoverUpdate(delta, autoAnimationForward);
}
/**
@@ -422,10 +488,11 @@ public class MenuButton {
image = anim.getCurrentFrame();
int xOffset = 0, yOffset = 0;
float currentScale = scale.getValue();
if (dir != Expand.CENTER) {
// offset by difference between normal/scaled image dimensions
xOffset = (int) ((scale - 1f) * image.getWidth());
yOffset = (int) ((scale - 1f) * image.getHeight());
xOffset = (int) ((currentScale - 1f) * image.getWidth());
yOffset = (int) ((currentScale - 1f) * image.getHeight());
if (dir == Expand.UP || dir == Expand.DOWN)
xOffset = 0; // no horizontal offset
if (dir == Expand.RIGHT || dir == Expand.LEFT)
@@ -435,7 +502,7 @@ public class MenuButton {
if (dir == Expand.DOWN || dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT)
yOffset *= -1; // flip y for down
}
this.xRadius = ((image.getWidth() * scale) + xOffset) / 2f;
this.yRadius = ((image.getHeight() * scale) + yOffset) / 2f;
this.xRadius = ((image.getWidth() * currentScale) + xOffset) / 2f;
this.yRadius = ((image.getHeight() * currentScale) + yOffset) / 2f;
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import org.newdawn.slick.Image;
/**
* Horizontal star stream.
*/
public class StarStream {
/** The container dimensions. */
private final int containerWidth, containerHeight;
/** The star image. */
private final Image starImg;
/** The current list of stars. */
private final List<Star> stars;
/** The maximum number of stars to draw at once. */
private static final int MAX_STARS = 20;
/** Random number generator instance. */
private final Random random;
/** Contains data for a single star. */
private class Star {
/** The star animation progress. */
private final AnimatedValue animatedValue;
/** The star properties. */
private final int distance, yOffset, angle;
/**
* Creates a star with the given properties.
* @param duration the time, in milliseconds, to show the star
* @param distance the distance for the star to travel in {@code duration}
* @param yOffset the vertical offset from the center of the container
* @param angle the rotation angle
* @param eqn the animation equation to use
*/
public Star(int duration, int distance, int yOffset, int angle, AnimationEquation eqn) {
this.animatedValue = new AnimatedValue(duration, 0f, 1f, eqn);
this.distance = distance;
this.yOffset = yOffset;
this.angle = angle;
}
/**
* Draws the star.
*/
public void draw() {
float t = animatedValue.getValue();
starImg.setImageColor(1f, 1f, 1f, Math.min((1 - t) * 5f, 1f));
starImg.drawEmbedded(
containerWidth - (distance * t), ((containerHeight - starImg.getHeight()) / 2) + yOffset,
starImg.getWidth(), starImg.getHeight(), angle);
}
/**
* Updates the animation by a delta interval.
* @param delta the delta interval since the last call
* @return true if an update was applied, false if the animation was not updated
*/
public boolean update(int delta) { return animatedValue.update(delta); }
}
/**
* Initializes the star stream.
* @param width the container width
* @param height the container height
*/
public StarStream(int width, int height) {
this.containerWidth = width;
this.containerHeight = height;
this.starImg = GameImage.STAR2.getImage().copy();
this.stars = new ArrayList<Star>();
this.random = new Random();
}
/**
* Draws the star stream.
*/
public void draw() {
if (stars.isEmpty())
return;
starImg.startUse();
for (Star star : stars)
star.draw();
starImg.endUse();
}
/**
* Updates the stars in the stream by a delta interval.
* @param delta the delta interval since the last call
*/
public void update(int delta) {
// update current stars
Iterator<Star> iter = stars.iterator();
while (iter.hasNext()) {
Star star = iter.next();
if (!star.update(delta))
iter.remove();
}
// create new stars
for (int i = stars.size(); i < MAX_STARS; i++) {
if (Math.random() < ((i < 5) ? 0.25 : 0.66))
break;
// generate star properties
float distanceRatio = Utils.clamp((float) getGaussian(0.65, 0.25), 0.2f, 0.925f);
int distance = (int) (containerWidth * distanceRatio);
int duration = (int) (distanceRatio * getGaussian(1300, 300));
int yOffset = (int) getGaussian(0, containerHeight / 20);
int angle = (int) getGaussian(0, 22.5);
AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD;
stars.add(new Star(duration, distance, angle, yOffset, eqn));
}
}
/**
* Clears the stars currently in the stream.
*/
public void clear() { stars.clear(); }
/**
* Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value
* with the given mean and standard deviation.
* @param mean the mean
* @param stdDev the standard deviation
*/
private double getGaussian(double mean, double stdDev) {
return mean + random.nextGaussian() * stdDev;
}
}

View File

@@ -21,11 +21,13 @@ package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
@@ -36,7 +38,6 @@ import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.StateBasedGame;
/**
@@ -62,7 +63,7 @@ public class UI {
private static int barNotifTimer = -1;
/** Duration, in milliseconds, to display bar notifications. */
private static final int BAR_NOTIFICATION_TIME = 1250;
private static final int BAR_NOTIFICATION_TIME = 1500;
/** The current tooltip. */
private static String tooltip;
@@ -70,11 +71,8 @@ public class UI {
/** Whether or not to check the current tooltip for line breaks. */
private static boolean tooltipNewlines;
/** The current tooltip timer. */
private static int tooltipTimer = -1;
/** Duration, in milliseconds, to fade tooltips. */
private static final int TOOLTIP_FADE_TIME = 200;
/** The alpha level of the current tooltip (if any). */
private static AnimatedValue tooltipAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
// game-related variables
private static GameContainer container;
@@ -87,10 +85,8 @@ public class UI {
* Initializes UI data.
* @param container the game container
* @param game the game object
* @throws SlickException
*/
public static void init(GameContainer container, StateBasedGame game)
throws SlickException {
public static void init(GameContainer container, StateBasedGame game) {
UI.container = container;
UI.input = container.getInput();
@@ -106,6 +102,8 @@ public class UI {
Image back = GameImage.MENU_BACK.getImage();
backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
}
backButton.setHoverAnimationDuration(350);
backButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT);
}
@@ -117,12 +115,11 @@ public class UI {
cursor.update(delta);
updateVolumeDisplay(delta);
updateBarNotification(delta);
if (tooltipTimer > 0)
tooltipTimer -= delta;
tooltipAlpha.update(-delta);
}
/**
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
* Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
* @param g the graphics context
*/
public static void draw(Graphics g) {
@@ -134,7 +131,7 @@ public class UI {
}
/**
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
* Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
* @param g the graphics context
* @param mouseX the mouse x coordinate
* @param mouseY the mouse y coordinate
@@ -178,18 +175,18 @@ public class UI {
*/
public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
Image tabImage = GameImage.MENU_TAB.getImage();
float tabTextX = x - (Utils.FONT_MEDIUM.getWidth(text) / 2);
float tabTextX = x - (Fonts.MEDIUM.getWidth(text) / 2);
float tabTextY = y - (tabImage.getHeight() / 2);
Color filter, textColor;
if (selected) {
filter = Color.white;
textColor = Color.black;
} else {
filter = (isHover) ? Utils.COLOR_RED_HOVER : Color.red;
filter = (isHover) ? Colors.RED_HOVER : Color.red;
textColor = Color.white;
}
tabImage.drawCentered(x, y, filter);
Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
Fonts.MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
}
/**
@@ -201,14 +198,14 @@ public class UI {
return;
String fps = String.format("%dFPS", container.getFPS());
Utils.FONT_BOLD.drawString(
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth(fps),
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight(fps),
Fonts.BOLD.drawString(
container.getWidth() * 0.997f - Fonts.BOLD.getWidth(fps),
container.getHeight() * 0.997f - Fonts.BOLD.getHeight(fps),
Integer.toString(container.getFPS()), Color.white
);
Utils.FONT_DEFAULT.drawString(
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth("FPS"),
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight("FPS"),
Fonts.DEFAULT.drawString(
container.getWidth() * 0.997f - Fonts.BOLD.getWidth("FPS"),
container.getHeight() * 0.997f - Fonts.BOLD.getHeight("FPS"),
"FPS", Color.white
);
}
@@ -263,7 +260,7 @@ public class UI {
*/
public static void changeVolume(int units) {
final float UNIT_OFFSET = 0.05f;
Options.setMasterVolume(container, Utils.getBoundedValue(Options.getMasterVolume(), UNIT_OFFSET * units, 0f, 1f));
Options.setMasterVolume(container, Utils.clamp(Options.getMasterVolume() + (UNIT_OFFSET * units), 0f, 1f));
if (volumeDisplay == -1)
volumeDisplay = 0;
else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
@@ -271,8 +268,9 @@ public class UI {
}
/**
* Draws loading progress (OSZ unpacking, beatmap parsing, sound loading)
* Draws loading progress (OSZ unpacking, beatmap parsing, replay importing, sound loading)
* at the bottom of the screen.
* @param g the graphics context
*/
public static void drawLoadingProgress(Graphics g) {
String text, file;
@@ -298,16 +296,16 @@ public class UI {
// draw loading info
float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
float lineY = container.getHeight() - marginY;
int lineOffsetY = Utils.FONT_MEDIUM.getLineHeight();
int lineOffsetY = Fonts.MEDIUM.getLineHeight();
if (Options.isLoadVerbose()) {
// verbose: display percentages and file names
Utils.FONT_MEDIUM.drawString(
Fonts.MEDIUM.drawString(
marginX, lineY - (lineOffsetY * 2),
String.format("%s (%d%%)", text, progress), Color.white);
Utils.FONT_MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
Fonts.MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
} else {
// draw loading bar
Utils.FONT_MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
Fonts.MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
g.setColor(Color.white);
g.fillRoundRect(marginX, lineY - (lineOffsetY / 2f),
(container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
@@ -357,12 +355,7 @@ public class UI {
if (s != null) {
tooltip = s;
tooltipNewlines = newlines;
if (tooltipTimer <= 0)
tooltipTimer = delta;
else
tooltipTimer += delta * 2;
if (tooltipTimer > TOOLTIP_FADE_TIME)
tooltipTimer = TOOLTIP_FADE_TIME;
tooltipAlpha.update(delta * 2);
}
}
@@ -372,26 +365,26 @@ public class UI {
* @param g the graphics context
*/
public static void drawTooltip(Graphics g) {
if (tooltipTimer <= 0 || tooltip == null)
if (tooltipAlpha.getTime() == 0 || tooltip == null)
return;
int containerWidth = container.getWidth(), containerHeight = container.getHeight();
int margin = containerWidth / 100, textMarginX = 2;
int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 2;
int lineHeight = Utils.FONT_SMALL.getLineHeight();
int lineHeight = Fonts.SMALL.getLineHeight();
int textWidth = textMarginX * 2, textHeight = lineHeight;
if (tooltipNewlines) {
String[] lines = tooltip.split("\\n");
int maxWidth = Utils.FONT_SMALL.getWidth(lines[0]);
int maxWidth = Fonts.SMALL.getWidth(lines[0]);
for (int i = 1; i < lines.length; i++) {
int w = Utils.FONT_SMALL.getWidth(lines[i]);
int w = Fonts.SMALL.getWidth(lines[i]);
if (w > maxWidth)
maxWidth = w;
}
textWidth += maxWidth;
textHeight += lineHeight * (lines.length - 1);
} else
textWidth += Utils.FONT_SMALL.getWidth(tooltip);
textWidth += Fonts.SMALL.getWidth(tooltip);
// get drawing coordinates
int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
@@ -405,29 +398,29 @@ public class UI {
y = margin;
// draw tooltip text inside a filled rectangle
float alpha = (float) tooltipTimer / TOOLTIP_FADE_TIME;
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = alpha;
g.setColor(Utils.COLOR_BLACK_ALPHA);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
float alpha = tooltipAlpha.getValue();
float oldAlpha = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = alpha;
g.setColor(Colors.BLACK_ALPHA);
Colors.BLACK_ALPHA.a = oldAlpha;
g.fillRect(x, y, textWidth, textHeight);
oldAlpha = Utils.COLOR_DARK_GRAY.a;
Utils.COLOR_DARK_GRAY.a = alpha;
g.setColor(Utils.COLOR_DARK_GRAY);
oldAlpha = Colors.DARK_GRAY.a;
Colors.DARK_GRAY.a = alpha;
g.setColor(Colors.DARK_GRAY);
g.setLineWidth(1);
g.drawRect(x, y, textWidth, textHeight);
Utils.COLOR_DARK_GRAY.a = oldAlpha;
oldAlpha = Utils.COLOR_WHITE_ALPHA.a;
Utils.COLOR_WHITE_ALPHA.a = alpha;
Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Utils.COLOR_WHITE_ALPHA);
Utils.COLOR_WHITE_ALPHA.a = oldAlpha;
Colors.DARK_GRAY.a = oldAlpha;
oldAlpha = Colors.WHITE_ALPHA.a;
Colors.WHITE_ALPHA.a = alpha;
Fonts.SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA);
Colors.WHITE_ALPHA.a = oldAlpha;
}
/**
* Resets the tooltip.
*/
public static void resetTooltip() {
tooltipTimer = -1;
tooltipAlpha.setTime(0);
tooltip = null;
}
@@ -475,18 +468,18 @@ public class UI {
if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
float barHeight = Utils.FONT_LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
float oldAlphaB = Utils.COLOR_BLACK_ALPHA.a, oldAlphaW = Utils.COLOR_WHITE_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a *= alpha;
Utils.COLOR_WHITE_ALPHA.a = alpha;
g.setColor(Utils.COLOR_BLACK_ALPHA);
float barHeight = Fonts.LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
float oldAlphaB = Colors.BLACK_ALPHA.a, oldAlphaW = Colors.WHITE_ALPHA.a;
Colors.BLACK_ALPHA.a *= alpha;
Colors.WHITE_ALPHA.a = alpha;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
Utils.FONT_LARGE.drawString(
midX - Utils.FONT_LARGE.getWidth(barNotif) / 2f,
midY - Utils.FONT_LARGE.getLineHeight() / 2.2f,
barNotif, Utils.COLOR_WHITE_ALPHA);
Utils.COLOR_BLACK_ALPHA.a = oldAlphaB;
Utils.COLOR_WHITE_ALPHA.a = oldAlphaW;
Fonts.LARGE.drawString(
midX - Fonts.LARGE.getWidth(barNotif) / 2f,
midY - Fonts.LARGE.getLineHeight() / 2.2f,
barNotif, Colors.WHITE_ALPHA);
Colors.BLACK_ALPHA.a = oldAlphaB;
Colors.WHITE_ALPHA.a = oldAlphaW;
}
/**

View File

@@ -0,0 +1,134 @@
/*
* 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.ui.animations;
import itdelatrisu.opsu.Utils;
/**
* Utility class for updating a value using an animation equation.
*/
public class AnimatedValue {
/** The animation duration, in milliseconds. */
private int duration;
/** The current time, in milliseconds. */
private int time;
/** The base value. */
private float base;
/** The maximum difference from the base value. */
private float diff;
/** The current value. */
private float value;
/** The animation equation to use. */
private AnimationEquation eqn;
/**
* Constructor.
* @param duration the total animation duration, in milliseconds
* @param min the minimum value
* @param max the maximum value
* @param eqn the animation equation to use
*/
public AnimatedValue(int duration, float min, float max, AnimationEquation eqn) {
this.time = 0;
this.duration = duration;
this.value = min;
this.base = min;
this.diff = max - min;
this.eqn = eqn;
}
/**
* Returns the current value.
*/
public float getValue() { return value; }
/**
* Returns the current animation time, in milliseconds.
*/
public int getTime() { return time; }
/**
* Sets the animation time manually.
* @param time the new time, in milliseconds
*/
public void setTime(int time) {
this.time = Utils.clamp(time, 0, duration);
updateValue();
}
/**
* Returns the total animation duration, in milliseconds.
*/
public int getDuration() { return duration; }
/**
* Sets the animation duration.
* @param duration the new duration, in milliseconds
*/
public void setDuration(int duration) {
this.duration = duration;
int newTime = Utils.clamp(time, 0, duration);
if (time != newTime) {
this.time = newTime;
updateValue();
}
}
/**
* Returns the animation equation being used.
*/
public AnimationEquation getEquation() { return eqn; }
/**
* Sets the animation equation to use.
* @param eqn the new equation
*/
public void setEquation(AnimationEquation eqn) {
this.eqn = eqn;
updateValue();
}
/**
* Updates the animation by a delta interval.
* @param delta the delta interval since the last call.
* @return true if an update was applied, false if the animation was not updated
*/
public boolean update(int delta) {
int newTime = Utils.clamp(time + delta, 0, duration);
if (time != newTime) {
this.time = newTime;
updateValue();
return true;
}
return false;
}
/**
* Recalculates the value by applying the animation equation with the current time.
*/
private void updateValue() {
float t = eqn.calc((float) time / duration);
this.value = base + (t * diff);
}
}

View File

@@ -0,0 +1,308 @@
/*
* 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.ui.animations;
/*
* These equations are copyright (c) 2001 Robert Penner, all rights reserved,
* and are open source under the BSD License.
* http://www.opensource.org/licenses/bsd-license.php
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of contributors may be used
* to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Easing functions for animations.
*
* @author Robert Penner (<a href="http://robertpenner.com/easing/">http://robertpenner.com/easing/</a>)
* @author CharlotteGore (<a href="https://github.com/CharlotteGore/functional-easing">https://github.com/CharlotteGore/functional-easing</a>)
*/
public enum AnimationEquation {
/* Linear */
LINEAR {
@Override
public float calc(float t) { return t; }
},
/* Quadratic */
IN_QUAD {
@Override
public float calc(float t) { return t * t; }
},
OUT_QUAD {
@Override
public float calc(float t) { return -1 * t * (t - 2); }
},
IN_OUT_QUAD {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t;
t = t - 1;
return -0.5f * (t * (t - 2) - 1);
}
},
/* Cubic */
IN_CUBIC {
@Override
public float calc(float t) { return t * t * t; }
},
OUT_CUBIC {
@Override
public float calc(float t) {
t = t - 1;
return t * t * t + 1;
}
},
IN_OUT_CUBIC {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t;
t = t - 2;
return 0.5f * (t * t * t + 2);
}
},
/* Quartic */
IN_QUART {
@Override
public float calc(float t) { return t * t * t * t; }
},
OUT_QUART {
@Override
public float calc(float t) {
t = t - 1;
return -1 * (t * t * t * t - 1);
}
},
IN_OUT_QUART {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t * t;
t = t - 2;
return -0.5f * (t * t * t * t - 2);
}
},
/* Quintic */
IN_QUINT {
@Override
public float calc(float t) { return t * t * t * t * t; }
},
OUT_QUINT {
@Override
public float calc(float t) {
t = t - 1;
return (t * t * t * t * t + 1);
}
},
IN_OUT_QUINT {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t * t * t;
t = t - 2;
return 0.5f * (t * t * t * t * t + 2);
}
},
/* Sine */
IN_SINE {
@Override
public float calc(float t) { return -1 * (float) Math.cos(t * (Math.PI / 2)) + 1; }
},
OUT_SINE {
@Override
public float calc(float t) { return (float) Math.sin(t * (Math.PI / 2)); }
},
IN_OUT_SINE {
@Override
public float calc(float t) { return (float) (Math.cos(Math.PI * t) - 1) / -2; }
},
/* Exponential */
IN_EXPO {
@Override
public float calc(float t) { return (t == 0) ? 0 : (float) Math.pow(2, 10 * (t - 1)); }
},
OUT_EXPO {
@Override
public float calc(float t) { return (t == 1) ? 1 : (float) -Math.pow(2, -10 * t) + 1; }
},
IN_OUT_EXPO {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
t = t * 2;
if (t < 1)
return 0.5f * (float) Math.pow(2, 10 * (t - 1));
t = t - 1;
return 0.5f * ((float) -Math.pow(2, -10 * t) + 2);
}
},
/* Circular */
IN_CIRC {
@Override
public float calc(float t) { return -1 * ((float) Math.sqrt(1 - t * t) - 1); }
},
OUT_CIRC {
@Override
public float calc(float t) {
t = t - 1;
return (float) Math.sqrt(1 - t * t);
}
},
IN_OUT_CIRC {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return -0.5f * ((float) Math.sqrt(1 - t * t) - 1);
t = t - 2;
return 0.5f * ((float) Math.sqrt(1 - t * t) + 1);
}
},
/* Back */
IN_BACK {
@Override
public float calc(float t) { return t * t * ((OVERSHOOT + 1) * t - OVERSHOOT); }
},
OUT_BACK {
@Override
public float calc(float t) {
t = t - 1;
return t * t * ((OVERSHOOT + 1) * t + OVERSHOOT) + 1;
}
},
IN_OUT_BACK {
@Override
public float calc(float t) {
float overshoot = OVERSHOOT * 1.525f;
t = t * 2;
if (t < 1)
return 0.5f * (t * t * ((overshoot + 1) * t - overshoot));
t = t - 2;
return 0.5f * (t * t * ((overshoot + 1) * t + overshoot) + 2);
}
},
/* Bounce */
IN_BOUNCE {
@Override
public float calc(float t) { return 1 - OUT_BOUNCE.calc(1 - t); }
},
OUT_BOUNCE {
@Override
public float calc(float t) {
if (t < 0.36363636f)
return 7.5625f * t * t;
else if (t < 0.72727273f) {
t = t - 0.54545454f;
return 7.5625f * t * t + 0.75f;
} else if (t < 0.90909091f) {
t = t - 0.81818182f;
return 7.5625f * t * t + 0.9375f;
} else {
t = t - 0.95454546f;
return 7.5625f * t * t + 0.984375f;
}
}
},
IN_OUT_BOUNCE {
@Override
public float calc(float t) {
if (t < 0.5f)
return IN_BOUNCE.calc(t * 2) * 0.5f;
return OUT_BOUNCE.calc(t * 2 - 1) * 0.5f + 0.5f;
}
},
/* Elastic */
IN_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.3f;
t = t - 1;
return -((float) Math.pow(2, 10 * t) * (float) Math.sin(((t - period / 4) * (Math.PI * 2)) / period));
}
},
OUT_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.3f;
return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) + 1;
}
},
IN_OUT_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.44999996f;
t = t * 2 - 1;
if (t < 0)
return -0.5f * ((float) Math.pow(2, 10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period));
return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) * 0.5f + 1;
}
};
/** Overshoot constant for "back" easings. */
private static final float OVERSHOOT = 1.70158f;
/**
* Calculates a new {@code t} value using the animation equation.
* @param t the raw {@code t} value [0,1]
* @return the new {@code t} value [0,1]
*/
public abstract float calc(float t);
}