From 8b9e230fa75c3178f59dfee28ffbf2f127b589c3 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Tue, 7 Jul 2015 18:37:03 -0500 Subject: [PATCH 01/34] Updated dependencies. - LWJGL is now at the final 2.x release version. - Slick2D is now at the final (?) release version. - Added dependency for org.tukaani.xz, since it becomes optional in commons-compress 1.9 and is needed for LZMA compression. Signed-off-by: Jeffrey Han --- pom.xml | 15 ++++++++++----- src/org/newdawn/slick/Image.java | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 2921441f..c3d33d3e 100644 --- a/pom.xml +++ b/pom.xml @@ -142,12 +142,12 @@ org.lwjgl.lwjgl lwjgl - 2.9.1 + 2.9.3 org.slick2d slick2d-core - 1.0.0 + 1.0.1 org.jcraft @@ -197,17 +197,22 @@ org.apache.maven maven-artifact - 3.0.3 + 3.3.3 org.apache.commons commons-compress - 1.8 + 1.9 + + + org.tukaani + xz + 1.5 com.github.jponge lzma-java - 1.2 + 1.3 diff --git a/src/org/newdawn/slick/Image.java b/src/org/newdawn/slick/Image.java index 1737a808..38d9e8be 100644 --- a/src/org/newdawn/slick/Image.java +++ b/src/org/newdawn/slick/Image.java @@ -595,6 +595,7 @@ public void draw(float x, float y) { * @param y The y location to draw the image at * @param filter The color to filter with when drawing */ + @Override public void draw(float x, float y, Color filter) { init(); draw(x,y,width,height, filter); @@ -719,6 +720,7 @@ public void draw(float x,float y,float scale,Color filter) { * @param height * The height to render the image at */ + @Override public void draw(float x,float y,float width,float height) { init(); draw(x,y,width,height,Color.white); @@ -797,6 +799,7 @@ public void drawSheared(float x,float y, float hshear, float vshear, Color filte * @param height The height to render the image at * @param filter The color to filter with while drawing */ + @Override public void draw(float x,float y,float width,float height,Color filter) { if (alpha != 1) { if (filter == null) { From 26ab61910e051dfb1dac5b8258b70bcf7df7ac8e Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Tue, 7 Jul 2015 19:03:54 -0500 Subject: [PATCH 02/34] Added option to disable automatic checking for updates. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Opsu.java | 20 +++++++++++--------- src/itdelatrisu/opsu/Options.java | 9 ++++++++- src/itdelatrisu/opsu/states/OptionsMenu.java | 3 ++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 1f89dd5b..09e7026f 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -142,16 +142,18 @@ public void uncaughtException(Thread t, Throwable e) { Updater.get().setUpdateInfo(args[0], args[1]); // check for updates - new Thread() { - @Override - public void run() { - try { - Updater.get().checkForUpdates(); - } catch (IOException e) { - Log.warn("Check for updates failed.", e); + if (!Options.isUpdaterDisabled()) { + new Thread() { + @Override + public void run() { + try { + Updater.get().checkForUpdates(); + } catch (IOException e) { + Log.warn("Check for updates failed.", e); + } } - } - }.start(); + }.start(); + } // start the game try { diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index dc81f1b0..4f15a3a7 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -454,7 +454,8 @@ public String getValueString() { } }, ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true), - REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false); + REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false), + DISABLE_UPDATER ("Disable Automatic Updates", "DisableUpdater", "Disable automatic checking for updates upon starting opsu!.", false); /** Option name. */ private String name; @@ -965,6 +966,12 @@ public static void setDisplayMode(Container app) { */ public static boolean isReplaySeekingEnabled() { return GameOption.REPLAY_SEEKING.getBooleanValue(); } + /** + * Returns whether or not automatic checking for updates is disabled. + * @return true if disabled + */ + public static boolean isUpdaterDisabled() { return GameOption.DISABLE_UPDATER.getBooleanValue(); } + /** * Sets the track checkpoint time, if within bounds. * @param time the track position (in ms) diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index 0c250719..b921e2ee 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -94,7 +94,8 @@ private enum OptionTab { GameOption.FIXED_AR, GameOption.FIXED_OD, GameOption.CHECKPOINT, - GameOption.REPLAY_SEEKING + GameOption.REPLAY_SEEKING, + GameOption.DISABLE_UPDATER }); /** Total number of tabs. */ From 420f1fb02cbdbad90c28ed5c484f118e499d3fcd Mon Sep 17 00:00:00 2001 From: Peter Tissen Date: Wed, 8 Jul 2015 16:18:41 +0200 Subject: [PATCH 03/34] Backup and restore the viewport size when rendering sliders. Needed because Slick tends to allocate offscreen buffers for itself only with power of two textures, so it will use another viewport when rendering to its own offscreen buffers. --- src/itdelatrisu/opsu/render/CurveRenderState.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index bd7a0914..1bd26bd3 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -24,6 +24,7 @@ import java.nio.ByteBuffer; import java.nio.FloatBuffer; +import java.nio.IntBuffer; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; @@ -114,9 +115,13 @@ public void draw(Color color, Color borderColor, Vec2f[] curve) { mapping = cache.insert(hitObject); fbo = mapping; - int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); - int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + int oldFb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); + int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + //glGetInteger requires a buffer of size 16, even though just 4 + //values are returned in this specific case + IntBuffer oldViewport = BufferUtils.createIntBuffer(16); + GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport); GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID()); GL11.glViewport(0, 0, fbo.width, fbo.height); GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); @@ -125,8 +130,9 @@ public void draw(Color color, Color borderColor, Vec2f[] curve) { this.draw_curve(color, borderColor, curve); color.a = 1f; - GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex); + GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, oldFb); + GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3)); Utils.COLOR_WHITE_FADE.a = alpha; } From 7941a70238ad2a54e5f69359e10e6ff0f7813eb6 Mon Sep 17 00:00:00 2001 From: Peter Tissen Date: Thu, 9 Jul 2015 18:37:39 +0200 Subject: [PATCH 04/34] use EXT version of FBOs instead of the ARB version This doesn't actually make a difference in functionality. The issue is that the flashlight mod uses Slicks FBO functions which use the EXT version and Intel drivers generate the same FBO IDs twice if the EXT and ARB versions are mixed. --- .../opsu/render/CurveRenderState.java | 7 ++-- src/itdelatrisu/opsu/render/Rendertarget.java | 32 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index 1bd26bd3..17a62e56 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -27,6 +27,7 @@ import java.nio.IntBuffer; import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.EXTFramebufferObject; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL14; @@ -115,14 +116,14 @@ public void draw(Color color, Color borderColor, Vec2f[] curve) { mapping = cache.insert(hitObject); fbo = mapping; - int oldFb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); + int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); //glGetInteger requires a buffer of size 16, even though just 4 //values are returned in this specific case IntBuffer oldViewport = BufferUtils.createIntBuffer(16); GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID()); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fbo.getID()); GL11.glViewport(0, 0, fbo.width, fbo.height); GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); @@ -131,7 +132,7 @@ public void draw(Color color, Color borderColor, Vec2f[] curve) { color.a = 1f; GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, oldFb); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb); GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3)); Utils.COLOR_WHITE_FADE.a = alpha; } diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java index c6f19387..a783b3ab 100644 --- a/src/itdelatrisu/opsu/render/Rendertarget.java +++ b/src/itdelatrisu/opsu/render/Rendertarget.java @@ -19,10 +19,8 @@ import java.nio.ByteBuffer; +import org.lwjgl.opengl.EXTFramebufferObject; import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL20; -import org.lwjgl.opengl.GL30; -import org.lwjgl.opengl.GL32; /** * Represents a rendertarget. For now this maps to an OpenGL FBO via LWJGL. @@ -50,16 +48,16 @@ public class Rendertarget { private Rendertarget(int width, int height) { this.width = width; this.height = height; - fboID = GL30.glGenFramebuffers(); + fboID = EXTFramebufferObject.glGenFramebuffersEXT(); textureID = GL11.glGenTextures(); - depthBufferID = GL30.glGenRenderbuffers(); + depthBufferID = EXTFramebufferObject.glGenRenderbuffersEXT(); } /** * Bind this rendertarget as the primary framebuffer. */ public void bind() { - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboID); } /** @@ -83,7 +81,7 @@ public int getTextureID() { * Bind the default framebuffer. */ public static void unbind() { - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0); + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0); } /** @@ -93,8 +91,9 @@ public static void unbind() { * @param height the height */ public static Rendertarget createRTTFramebuffer(int width, int height) { - int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING); + int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + int old_drawbuffer = GL11.glGetInteger(GL11.GL_DRAW_BUFFER); Rendertarget buffer = new Rendertarget(width,height); buffer.bind(); @@ -104,16 +103,15 @@ public static Rendertarget createRTTFramebuffer(int width, int height) { GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, buffer.depthBufferID); - GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height); - GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, buffer.depthBufferID); + EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID); + EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, GL11.GL_DEPTH_COMPONENT, width, height); + EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID); - GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0); - GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0); + EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fboTexture, 0); GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture); - GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer); - + EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, old_framebuffer); + return buffer; } @@ -122,8 +120,8 @@ public static Rendertarget createRTTFramebuffer(int width, int height) { * to use this rendertarget with OpenGL after calling this method. */ public void destroyRTT() { - GL30.glDeleteFramebuffers(fboID); - GL30.glDeleteRenderbuffers(depthBufferID); + EXTFramebufferObject.glDeleteFramebuffersEXT(fboID); + EXTFramebufferObject.glDeleteRenderbuffersEXT(depthBufferID); GL11.glDeleteTextures(textureID); } } From 05c7ac0a02d1f85927a39a0b7d29fb6ed31e9595 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 9 Jul 2015 11:59:53 -0500 Subject: [PATCH 05/34] Minor follow-up to #108. Set minimum OpenGL version for mmsliders to 3.0 (from 3.2) and removed an unused variable. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/objects/curves/Curve.java | 2 +- src/itdelatrisu/opsu/render/Rendertarget.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 0d2f4609..74ad3864 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -87,7 +87,7 @@ public static void init(int width, int height, float circleSize, Color borderCol Curve.borderColor = borderColor; ContextCapabilities capabilities = GLContext.getCapabilities(); - mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL32; + mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL30; if (mmsliderSupported) CurveRenderState.init(width, height, circleSize); else { diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java index a783b3ab..abd16b9d 100644 --- a/src/itdelatrisu/opsu/render/Rendertarget.java +++ b/src/itdelatrisu/opsu/render/Rendertarget.java @@ -93,7 +93,6 @@ public static void unbind() { public static Rendertarget createRTTFramebuffer(int width, int height) { int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); - int old_drawbuffer = GL11.glGetInteger(GL11.GL_DRAW_BUFFER); Rendertarget buffer = new Rendertarget(width,height); buffer.bind(); @@ -111,7 +110,7 @@ public static Rendertarget createRTTFramebuffer(int width, int height) { GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture); EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, old_framebuffer); - + return buffer; } From 4e2074e41bc399c848850bfebc2af79b5579ed9a Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 11 Jul 2015 10:51:52 -0500 Subject: [PATCH 06/34] Show errors if any directories could not be created. (fixes #97) Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Options.java | 22 +++++++++++++++------- src/itdelatrisu/opsu/Utils.java | 8 +++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 4f15a3a7..a4b84447 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -140,8 +140,8 @@ private static File getXDGBaseDir(String env, String fallback) { rootPath = String.format("%s/%s", home, fallback); } File dir = new File(rootPath, "opsu"); - if (!dir.isDirectory()) - dir.mkdir(); + if (!dir.isDirectory() && !dir.mkdir()) + ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false); return dir; } else return new File("./"); @@ -357,7 +357,7 @@ public void drag(GameContainer container, int d) { public String getValueString() { return String.format("%dms", val); } }, DISABLE_SOUNDS ("Disable All Sound Effects", "DisableSound", "May resolve Linux sound driver issues. Requires a restart.", - (System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)), + (System.getProperty("os.name").toLowerCase().contains("linux"))), KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") { @Override public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); } @@ -1094,7 +1094,10 @@ public static File getBeatmapDir() { if (beatmapDir.isDirectory()) return beatmapDir; } - beatmapDir.mkdir(); // none found, create new directory + + // none found, create new directory + if (!beatmapDir.mkdir()) + ErrorHandler.error(String.format("Failed to create beatmap directory at '%s'.", beatmapDir.getAbsolutePath()), null, false); return beatmapDir; } @@ -1108,7 +1111,8 @@ public static File getOSZDir() { return oszDir; oszDir = new File(DATA_DIR, "SongPacks/"); - oszDir.mkdir(); + if (!oszDir.isDirectory() && !oszDir.mkdir()) + ErrorHandler.error(String.format("Failed to create song packs directory at '%s'.", oszDir.getAbsolutePath()), null, false); return oszDir; } @@ -1122,7 +1126,8 @@ public static File getReplayImportDir() { return replayImportDir; replayImportDir = new File(DATA_DIR, "ReplayImport/"); - replayImportDir.mkdir(); + if (!replayImportDir.isDirectory() && !replayImportDir.mkdir()) + ErrorHandler.error(String.format("Failed to create replay import directory at '%s'.", replayImportDir.getAbsolutePath()), null, false); return replayImportDir; } @@ -1167,7 +1172,10 @@ public static File getSkinRootDir() { if (skinRootDir.isDirectory()) return skinRootDir; } - skinRootDir.mkdir(); // none found, create new directory + + // none found, create new directory + if (!skinRootDir.mkdir()) + ErrorHandler.error(String.format("Failed to create skins directory at '%s'.", skinRootDir.getAbsolutePath()), null, false); return skinRootDir; } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index feb24d44..9e24be47 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -289,11 +289,9 @@ public static boolean isGameKeyPressed() { public static void takeScreenShot() { // create the screenshot directory File dir = Options.getScreenshotDir(); - if (!dir.isDirectory()) { - if (!dir.mkdir()) { - ErrorHandler.error("Failed to create screenshot directory.", null, false); - return; - } + if (!dir.isDirectory() && !dir.mkdir()) { + ErrorHandler.error(String.format("Failed to create screenshot directory at '%s'.", dir.getAbsolutePath()), null, false); + return; } // create file name From 5dac21a5458bb2f3f963926f642a47d69628b72b Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 16 Jul 2015 18:14:46 -0500 Subject: [PATCH 07/34] Cursor trail now considers actual FPS (not target FPS). (fixes #109) Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/ui/Cursor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/itdelatrisu/opsu/ui/Cursor.java b/src/itdelatrisu/opsu/ui/Cursor.java index 252a9c2c..a35c405c 100644 --- a/src/itdelatrisu/opsu/ui/Cursor.java +++ b/src/itdelatrisu/opsu/ui/Cursor.java @@ -125,7 +125,7 @@ public void draw(int mouseX, int mouseY, boolean mousePressed) { cursorMiddle = GameImage.CURSOR_MIDDLE.getImage(); int removeCount = 0; - int FPSmod = (Options.getTargetFPS() / 60); + float FPSmod = Math.max(container.getFPS(), 1) / 60f; Skin skin = Options.getSkin(); // scale cursor @@ -151,13 +151,13 @@ public void draw(int mouseX, int mouseY, boolean mousePressed) { lastX = mouseX; lastY = mouseY; - removeCount = (cursorX.size() / (6 * FPSmod)) + 1; + removeCount = (int) (cursorX.size() / (6 * FPSmod)) + 1; } else { // old style: sample one point at a time cursorX.add(mouseX); cursorY.add(mouseY); - int max = 10 * FPSmod; + int max = (int) (10 * FPSmod); if (cursorX.size() > max) removeCount = cursorX.size() - max; } From 6a4c6a8d37844a8263bad8537c4227f991ff2f05 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 18 Jul 2015 23:55:06 -0500 Subject: [PATCH 08/34] Updating to version 0.10.1. Signed-off-by: Jeffrey Han --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c3d33d3e..bfaaef28 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 itdelatrisu opsu - 0.10.0 + 0.10.1 ${maven.build.timestamp} yyyy-MM-dd HH:mm From c91146b024fb218405c460eca007563db9d38e65 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Wed, 5 Aug 2015 22:28:14 -0500 Subject: [PATCH 09/34] Added easing functions for all-around better animations. These are Robert Penner's easing functions (http://robertpenner.com/easing/), refactored by CharlotteGore to only take a t parameter (https://github.com/CharlotteGore/functional-easing). Licensed under BSD (the former) and MIT (the latter). Related changes: - Added "AnimatedValue" utility class for updating values used in animations. - MenuButton now uses AnimatedValue to handle its animations (still linear by default). - Added in-out-back easings on logo, mods, and various other elements; added out-bounce easings on button menu. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/GameMod.java | 3 + src/itdelatrisu/opsu/Utils.java | 16 + src/itdelatrisu/opsu/states/ButtonMenu.java | 24 +- src/itdelatrisu/opsu/states/Game.java | 3 + .../opsu/states/GamePauseMenu.java | 9 + src/itdelatrisu/opsu/states/MainMenu.java | 20 +- src/itdelatrisu/opsu/ui/MenuButton.java | 155 +++++---- src/itdelatrisu/opsu/ui/UI.java | 3 + .../opsu/ui/animations/AnimatedValue.java | 124 +++++++ .../opsu/ui/animations/AnimationEquation.java | 309 ++++++++++++++++++ 10 files changed, 588 insertions(+), 78 deletions(-) create mode 100644 src/itdelatrisu/opsu/ui/animations/AnimatedValue.java create mode 100644 src/itdelatrisu/opsu/ui/animations/AnimationEquation.java diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index ff7c86a0..e6adb54c 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -19,6 +19,7 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.ui.MenuButton; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.util.Arrays; import java.util.Collections; @@ -199,6 +200,8 @@ public static void init(int width, int height) { mod.button = new MenuButton(img, baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f, mod.category.getY()); + mod.button.setHoverAnimationDuration(300); + mod.button.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); mod.button.setHoverExpand(1.2f); mod.button.setHoverRotate(10f); diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 9e24be47..1737a10e 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -239,6 +239,22 @@ else if (val > max) return val; } + /** + * Clamps a value between a lower and upper bound. + * @param val the value to clamp + * @param low the lower bound + * @param high the upper bound + * @return the clamped value + * @author fluddokt + */ + public static int clamp(int val, int low, int high) { + if (val < low) + return low; + if (val > high) + return high; + return val; + } + /** * Clamps a value between a lower and upper bound. * @param val the value to clamp diff --git a/src/itdelatrisu/opsu/states/ButtonMenu.java b/src/itdelatrisu/opsu/states/ButtonMenu.java index c27f0b67..de363c57 100644 --- a/src/itdelatrisu/opsu/states/ButtonMenu.java +++ b/src/itdelatrisu/opsu/states/ButtonMenu.java @@ -31,6 +31,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetNode; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.util.ArrayList; import java.util.List; @@ -261,8 +263,11 @@ public void scroll(GameContainer container, StateBasedGame game, int newValue) { /** The actual title string list, generated upon entering the state. */ private List actualTitle; + /** The horizontal center offset, used for the initial button animation. */ + private AnimatedValue centerOffset; + /** Initial x coordinate offsets left/right of center (for shifting animation), times width. (TODO) */ - private static final float OFFSET_WIDTH_RATIO = 1 / 18f; + private static final float OFFSET_WIDTH_RATIO = 1 / 25f; /** * Constructor. @@ -336,18 +341,14 @@ public void draw(GameContainer container, StateBasedGame game, Graphics g) { */ public void update(GameContainer container, int delta, int mouseX, int mouseY) { float center = container.getWidth() / 2f; + boolean centerOffsetUpdated = centerOffset.update(delta); + float centerOffsetX = centerOffset.getValue(); for (int i = 0; i < buttons.length; i++) { menuButtons[i].hoverUpdate(delta, mouseX, mouseY); // move button to center - float x = menuButtons[i].getX(); - if (i % 2 == 0) { - if (x < center) - menuButtons[i].setX(Math.min(x + (delta / 5f), center)); - } else { - if (x > center) - menuButtons[i].setX(Math.max(x - (delta / 5f), center)); - } + if (centerOffsetUpdated) + menuButtons[i].setX((i % 2 == 0) ? center + centerOffsetX : center - centerOffsetX); } } @@ -404,9 +405,10 @@ public void scroll(GameContainer container, StateBasedGame game, int newValue) { */ public void enter(GameContainer container, StateBasedGame game) { float center = container.getWidth() / 2f; - float centerOffset = container.getWidth() * OFFSET_WIDTH_RATIO; + float centerOffsetX = container.getWidth() * OFFSET_WIDTH_RATIO; + centerOffset = new AnimatedValue(700, centerOffsetX, 0, AnimationEquation.OUT_BOUNCE); for (int i = 0; i < buttons.length; i++) { - menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffset * -1 : centerOffset)); + menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffsetX : centerOffsetX * -1)); menuButtons[i].resetHover(); } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 6d2f0cdd..bfc65705 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -48,6 +48,7 @@ import itdelatrisu.opsu.replay.ReplayFrame; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; import java.util.LinkedList; @@ -1425,6 +1426,8 @@ private void loadImages() { Image skip = GameImage.SKIP.getImage(); skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f)); } + skipButton.setHoverAnimationDuration(350); + skipButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT); // load other images... diff --git a/src/itdelatrisu/opsu/states/GamePauseMenu.java b/src/itdelatrisu/opsu/states/GamePauseMenu.java index e73e5a2a..7a01c492 100644 --- a/src/itdelatrisu/opsu/states/GamePauseMenu.java +++ b/src/itdelatrisu/opsu/states/GamePauseMenu.java @@ -27,6 +27,7 @@ import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import org.lwjgl.input.Keyboard; import org.newdawn.slick.Color; @@ -227,6 +228,14 @@ public void loadImages() { continueButton = new MenuButton(GameImage.PAUSE_CONTINUE.getImage(), width / 2f, height * 0.25f); retryButton = new MenuButton(GameImage.PAUSE_RETRY.getImage(), width / 2f, height * 0.5f); backButton = new MenuButton(GameImage.PAUSE_BACK.getImage(), width / 2f, height * 0.75f); + final int buttonAnimationDuration = 300; + continueButton.setHoverAnimationDuration(buttonAnimationDuration); + retryButton.setHoverAnimationDuration(buttonAnimationDuration); + backButton.setHoverAnimationDuration(buttonAnimationDuration); + final AnimationEquation buttonAnimationEquation = AnimationEquation.IN_OUT_BACK; + continueButton.setHoverAnimationEquation(buttonAnimationEquation); + retryButton.setHoverAnimationEquation(buttonAnimationEquation); + backButton.setHoverAnimationEquation(buttonAnimationEquation); continueButton.setHoverExpand(); retryButton.setHoverExpand(); backButton.setHoverExpand(); diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index be484887..dd022368 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -33,6 +33,7 @@ import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton.Expand; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import itdelatrisu.opsu.ui.UI; import java.awt.Desktop; @@ -145,9 +146,18 @@ public void init(GameContainer container, StateBasedGame game) exitButton = new MenuButton(exitImg, width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f) ); - logo.setHoverExpand(1.05f); - playButton.setHoverExpand(1.05f); - exitButton.setHoverExpand(1.05f); + final int logoAnimationDuration = 350; + logo.setHoverAnimationDuration(logoAnimationDuration); + playButton.setHoverAnimationDuration(logoAnimationDuration); + exitButton.setHoverAnimationDuration(logoAnimationDuration); + final AnimationEquation logoAnimationEquation = AnimationEquation.IN_OUT_BACK; + logo.setHoverAnimationEquation(logoAnimationEquation); + playButton.setHoverAnimationEquation(logoAnimationEquation); + exitButton.setHoverAnimationEquation(logoAnimationEquation); + final float logoHoverScale = 1.1f; + logo.setHoverExpand(logoHoverScale); + playButton.setHoverExpand(logoHoverScale); + exitButton.setHoverExpand(logoHoverScale); // initialize music buttons int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth(); @@ -170,6 +180,8 @@ public void init(GameContainer container, StateBasedGame game) // initialize downloads button Image dlImg = GameImage.DOWNLOADS.getImage(); downloadsButton = new MenuButton(dlImg, width - dlImg.getWidth() / 2f, height / 2f); + downloadsButton.setHoverAnimationDuration(350); + downloadsButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); downloadsButton.setHoverExpand(1.03f, Expand.LEFT); // initialize repository button @@ -179,6 +191,8 @@ public void init(GameContainer container, StateBasedGame game) repoButton = new MenuButton(repoImg, startX - repoImg.getWidth(), startY - repoImg.getHeight() ); + repoButton.setHoverAnimationDuration(350); + repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK); repoButton.setHoverExpand(); startX -= repoImg.getWidth() * 1.75f; } else diff --git a/src/itdelatrisu/opsu/ui/MenuButton.java b/src/itdelatrisu/opsu/ui/MenuButton.java index 7370d0bc..3491cdf3 100644 --- a/src/itdelatrisu/opsu/ui/MenuButton.java +++ b/src/itdelatrisu/opsu/ui/MenuButton.java @@ -18,7 +18,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 +64,23 @@ 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; + + /** 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 +88,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. @@ -192,15 +208,15 @@ public void draw(Color filter) { 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 +233,10 @@ public void draw(Color filter) { 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 +284,61 @@ public boolean contains(float cx, float cy, float alpha) { */ 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); } /** * 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; + } + + /** + * 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 +346,45 @@ public void resetHover() { * 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 +421,21 @@ public void hoverUpdate(int delta, boolean isHover) { 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); } /** @@ -422,10 +448,11 @@ private void setHoverRadius() { 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 +462,7 @@ private void setHoverRadius() { 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; } } diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index 91ee4c9a..ac198af0 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -26,6 +26,7 @@ import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.replay.ReplayImporter; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import javax.swing.JOptionPane; import javax.swing.UIManager; @@ -106,6 +107,8 @@ public static void init(GameContainer container, StateBasedGame game) 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); } diff --git a/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java new file mode 100644 index 00000000..5e1636f9 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java @@ -0,0 +1,124 @@ +/* + * 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.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(); + } + + /** + * 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(); + } + } + + /** + * 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.getBoundedValue(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); + } +} diff --git a/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java new file mode 100644 index 00000000..a30c5b30 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/animations/AnimationEquation.java @@ -0,0 +1,309 @@ +/* + * 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.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 (http://robertpenner.com/easing/) + * @author CharlotteGore (https://github.com/CharlotteGore/functional-easing) + */ +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); +} From c0b3da37c2fd92ea97521e5ea0ab33f4b94b65ac Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 6 Aug 2015 00:53:30 -0500 Subject: [PATCH 10/34] Changed more messy animations to use AnimatedValue. Also finally refactored the main menu logo controller code... Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/objects/Spinner.java | 2 +- src/itdelatrisu/opsu/states/MainMenu.java | 125 +++++++++++++--------- src/itdelatrisu/opsu/states/SongMenu.java | 42 ++++---- src/itdelatrisu/opsu/states/Splash.java | 19 ++-- 4 files changed, 106 insertions(+), 82 deletions(-) diff --git a/src/itdelatrisu/opsu/objects/Spinner.java b/src/itdelatrisu/opsu/objects/Spinner.java index 3f9cdcab..dfd138ba 100644 --- a/src/itdelatrisu/opsu/objects/Spinner.java +++ b/src/itdelatrisu/opsu/objects/Spinner.java @@ -162,7 +162,7 @@ public Spinner(HitObject hitObject, Game game, GameData data) { final int maxVel = 48; final int minTime = 2000; final int maxTime = 5000; - maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime) + maxStoredDeltaAngles = Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime) * (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel); storedDeltaAngle = new float[maxStoredDeltaAngles]; diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index dd022368..383557d3 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -33,8 +33,9 @@ import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton.Expand; -import itdelatrisu.opsu.ui.animations.AnimationEquation; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.awt.Desktop; import java.io.IOException; @@ -62,7 +63,7 @@ */ public class MainMenu extends BasicGameState { /** Idle time, in milliseconds, before returning the logo to its original position. */ - private static final short MOVE_DELAY = 5000; + private static final short LOGO_IDLE_DELAY = 10000; /** Max alpha level of the menu background. */ private static final float BG_MAX_ALPHA = 0.9f; @@ -70,12 +71,21 @@ public class MainMenu extends BasicGameState { /** Logo button that reveals other buttons on click. */ private MenuButton logo; - /** Whether or not the logo has been clicked. */ - private boolean logoClicked = false; + /** Logo states. */ + private enum LogoState { DEFAULT, OPENING, OPEN, CLOSING } + + /** Current logo state. */ + private LogoState logoState = LogoState.DEFAULT; /** Delay timer, in milliseconds, before starting to move the logo back to the center. */ private int logoTimer = 0; + /** Logo horizontal offset for opening and closing actions. */ + private AnimatedValue logoOpen, logoClose; + + /** Logo button alpha levels. */ + private AnimatedValue logoButtonAlpha; + /** Main "Play" and "Exit" buttons. */ private MenuButton playButton, exitButton; @@ -98,7 +108,7 @@ public class MainMenu extends BasicGameState { private Stack previous; /** Background alpha level (for fade-in effect). */ - private float bgAlpha = 0f; + private AnimatedValue bgAlpha = new AnimatedValue(1100, 0f, BG_MAX_ALPHA, AnimationEquation.LINEAR); /** Whether or not a notification was already sent upon entering. */ private boolean enterNotification = false; @@ -154,7 +164,7 @@ public void init(GameContainer container, StateBasedGame game) logo.setHoverAnimationEquation(logoAnimationEquation); playButton.setHoverAnimationEquation(logoAnimationEquation); exitButton.setHoverAnimationEquation(logoAnimationEquation); - final float logoHoverScale = 1.1f; + final float logoHoverScale = 1.08f; logo.setHoverExpand(logoHoverScale); playButton.setHoverExpand(logoHoverScale); exitButton.setHoverExpand(logoHoverScale); @@ -203,6 +213,12 @@ public void init(GameContainer container, StateBasedGame game) updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight()); updateButton.setHoverExpand(1.15f); + // logo animations + float centerOffsetX = container.getWidth() / 5f; + logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD); + logoClose = new AnimatedValue(2200, centerOffsetX, 0, AnimationEquation.OUT_QUAD); + logoButtonAlpha = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR); + reset(); } @@ -215,11 +231,11 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) // draw background Beatmap beatmap = MusicController.getBeatmap(); if (Options.isDynamicBackgroundEnabled() && - beatmap != null && beatmap.drawBG(width, height, bgAlpha, true)) + beatmap != null && beatmap.drawBG(width, height, bgAlpha.getValue(), true)) ; else { Image bg = GameImage.MENU_BG.getImage(); - bg.setAlpha(bgAlpha); + bg.setAlpha(bgAlpha.getValue()); bg.draw(); } @@ -235,7 +251,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) downloadsButton.draw(); // draw buttons - if (logoTimer > 0) { + if (logoState == LogoState.OPEN || logoState == LogoState.CLOSING) { playButton.draw(); exitButton.draw(); } @@ -336,46 +352,44 @@ public void update(GameContainer container, StateBasedGame game, int delta) MusicController.toggleTrackDimmed(0.33f); // fade in background - if (bgAlpha < BG_MAX_ALPHA) { - bgAlpha += delta / 1000f; - if (bgAlpha > BG_MAX_ALPHA) - bgAlpha = BG_MAX_ALPHA; - } + bgAlpha.update(delta); // buttons - if (logoClicked) { - if (logoTimer == 0) { // shifting to left - if (logo.getX() > container.getWidth() / 3.3f) - logo.setX(logo.getX() - delta); - else - logoTimer = 1; - } else if (logoTimer >= MOVE_DELAY) // timer over: shift back to center - logoClicked = false; - else { // increment timer - logoTimer += delta; - if (logoTimer <= 500) { - // fade in buttons - playButton.getImage().setAlpha(logoTimer / 400f); - exitButton.getImage().setAlpha(logoTimer / 400f); - } - } - } else { - // fade out buttons - if (logoTimer > 0) { - float alpha = playButton.getImage().getAlpha(); - if (alpha > 0f) { - playButton.getImage().setAlpha(alpha - (delta / 200f)); - exitButton.getImage().setAlpha(alpha - (delta / 200f)); - } else - logoTimer = 0; + int centerX = container.getWidth() / 2; + float currentLogoButtonAlpha; + switch (logoState) { + case DEFAULT: + break; + case OPENING: + if (logoOpen.update(delta)) // shifting to left + logo.setX(centerX - logoOpen.getValue()); + else { + logoState = LogoState.OPEN; + logoTimer = 0; + logoButtonAlpha.setTime(0); } - - // move back to original location - if (logo.getX() < container.getWidth() / 2) { - logo.setX(logo.getX() + (delta / 3f)); - if (logo.getX() > container.getWidth() / 2) - logo.setX(container.getWidth() / 2); + break; + case OPEN: + if (logoButtonAlpha.update(delta)) { // fade in buttons + currentLogoButtonAlpha = logoButtonAlpha.getValue(); + playButton.getImage().setAlpha(currentLogoButtonAlpha); + exitButton.getImage().setAlpha(currentLogoButtonAlpha); + } else if (logoTimer >= LOGO_IDLE_DELAY) { // timer over: shift back to center + logoState = LogoState.CLOSING; + logoClose.setTime(0); + logoTimer = 0; + } else // increment timer + logoTimer += delta; + break; + case CLOSING: + if (logoButtonAlpha.update(-delta)) { // fade out buttons + currentLogoButtonAlpha = logoButtonAlpha.getValue(); + playButton.getImage().setAlpha(currentLogoButtonAlpha); + exitButton.getImage().setAlpha(currentLogoButtonAlpha); } + if (logoClose.update(delta)) // shifting to right + logo.setX(centerX - logoClose.getValue()); + break; } // tooltips @@ -471,7 +485,7 @@ public void mousePressed(int button, int x, int y) { SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU); menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false); if (Options.isDynamicBackgroundEnabled()) - bgAlpha = 0f; + bgAlpha.setTime(0); } else MusicController.setPosition(0); UI.sendBarNotification("<< Previous"); @@ -511,9 +525,10 @@ else if (Updater.get().showButton() && updateButton.contains(x, y)) { } // start moving logo (if clicked) - else if (!logoClicked) { + else if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) { if (logo.contains(x, y, 0.25f)) { - logoClicked = true; + logoState = LogoState.OPENING; + logoOpen.setTime(0); logoTimer = 0; playButton.getImage().setAlpha(0f); exitButton.getImage().setAlpha(0f); @@ -522,7 +537,7 @@ else if (!logoClicked) { } // other button actions (if visible) - else if (logoClicked) { + else if (logoState == LogoState.OPEN || logoState == LogoState.OPENING) { if (logo.contains(x, y, 0.25f) || playButton.contains(x, y, 0.25f)) { SoundController.playSound(SoundEffect.MENUHIT); enterSongMenu(); @@ -546,8 +561,9 @@ public void keyPressed(int key, char c) { break; case Input.KEY_P: SoundController.playSound(SoundEffect.MENUHIT); - if (!logoClicked) { - logoClicked = true; + if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) { + logoState = LogoState.OPENING; + logoOpen.setTime(0); logoTimer = 0; playButton.getImage().setAlpha(0f); exitButton.getImage().setAlpha(0f); @@ -595,8 +611,11 @@ private boolean musicPositionBarContains(float cx, float cy) { public void reset() { // reset logo logo.setX(container.getWidth() / 2); - logoClicked = false; + logoOpen.setTime(0); + logoClose.setTime(0); + logoButtonAlpha.setTime(0); logoTimer = 0; + logoState = LogoState.DEFAULT; logo.resetHover(); playButton.resetHover(); @@ -625,7 +644,7 @@ private void nextTrack() { previous.add(node.index); } if (Options.isDynamicBackgroundEnabled() && !sameAudio && !MusicController.isThemePlaying()) - bgAlpha = 0f; + bgAlpha.setTime(0); } /** diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index d191d125..681035d6 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -41,6 +41,8 @@ import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; import java.util.Map; @@ -138,8 +140,8 @@ public SongNode(BeatmapSetNode node, int index) { /** Button coordinate values. */ private float buttonX, buttonY, buttonOffset, buttonWidth, buttonHeight; - /** Current x offset of song buttons for mouse hover, in pixels. */ - private float hoverOffset = 0f; + /** Horizontal offset of song buttons for mouse hover, in pixels. */ + private AnimatedValue hoverOffset = new AnimatedValue(250, 0, MAX_HOVER_OFFSET, AnimationEquation.OUT_QUART); /** Current index of hovered song button. */ private int hoverIndex = -1; @@ -307,7 +309,7 @@ public void render(GameContainer container, StateBasedGame game, Graphics g) g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY)); for (int i = songButtonIndex; i <= MAX_SONG_BUTTONS && node != null; i++, node = node.next) { // draw the node - float offset = (i == hoverIndex) ? hoverOffset : 0f; + float offset = (i == hoverIndex) ? hoverOffset.getValue() : 0f; ScoreData[] scores = getScoreDataForNode(node, false); node.draw(buttonX - offset, buttonY + (i*buttonOffset) + DIVIDER_LINE_WIDTH / 2, (scores == null) ? Grade.NULL : scores[0].getGrade(), (node == focusNode)); @@ -560,15 +562,11 @@ public void update(GameContainer container, StateBasedGame game, int delta) float cx = (node.index == BeatmapSetList.get().getExpandedIndex()) ? buttonX * 0.9f : buttonX; if ((mouseX > cx && mouseX < cx + buttonWidth) && (mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) { - if (i == hoverIndex) { - if (hoverOffset < MAX_HOVER_OFFSET) { - hoverOffset += delta / 3f; - if (hoverOffset > MAX_HOVER_OFFSET) - hoverOffset = MAX_HOVER_OFFSET; - } - } else { + if (i == hoverIndex) + hoverOffset.update(delta); + else { hoverIndex = i; - hoverOffset = 0f; + hoverOffset.setTime(0); } isHover = true; break; @@ -576,7 +574,7 @@ public void update(GameContainer container, StateBasedGame game, int delta) } } if (!isHover) { - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; } else return; @@ -660,7 +658,7 @@ public void mousePressed(int button, int x, int y) { float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX; if ((x > cx && x < cx + buttonWidth) && (y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) { - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; // clicked node is already expanded @@ -685,7 +683,7 @@ public void mousePressed(int button, int x, int y) { } // restore hover data - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; // open beatmap menu @@ -820,11 +818,11 @@ public void keyPressed(int key, char c) { if (next != null) { SoundController.playSound(SoundEffect.MENUCLICK); BeatmapSetNode oldStartNode = startNode; - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; setFocus(next, 0, false, true); if (startNode == oldStartNode) { - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; } } @@ -836,11 +834,11 @@ public void keyPressed(int key, char c) { if (prev != null) { SoundController.playSound(SoundEffect.MENUCLICK); BeatmapSetNode oldStartNode = startNode; - float oldHoverOffset = hoverOffset; + int oldHoverOffsetTime = hoverOffset.getTime(); int oldHoverIndex = hoverIndex; setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true); if (startNode == oldStartNode) { - hoverOffset = oldHoverOffset; + hoverOffset.setTime(oldHoverOffsetTime); hoverIndex = oldHoverIndex; } } @@ -938,7 +936,7 @@ public void enter(GameContainer container, StateBasedGame game) selectRandomButton.resetHover(); selectMapOptionsButton.resetHover(); selectOptionsButton.resetHover(); - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; startScore = 0; beatmapMenuTimer = -1; @@ -1076,7 +1074,7 @@ else if (startNode.next != null) oldFocusNode = null; randomStack = new Stack(); songInfo = null; - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; search.setText(""); searchTimer = SEARCH_DELAY; @@ -1157,7 +1155,7 @@ private void changeIndex(int shift) { break; } if (shifted) { - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; } return; @@ -1175,7 +1173,7 @@ public BeatmapSetNode setFocus(BeatmapSetNode node, int beatmapIndex, boolean ch if (node == null) return null; - hoverOffset = 0f; + hoverOffset.setTime(0); hoverIndex = -1; songInfo = null; BeatmapSetNode oldFocus = focusNode; diff --git a/src/itdelatrisu/opsu/states/Splash.java b/src/itdelatrisu/opsu/states/Splash.java index 14d8245a..c83db4a0 100644 --- a/src/itdelatrisu/opsu/states/Splash.java +++ b/src/itdelatrisu/opsu/states/Splash.java @@ -29,13 +29,14 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.replay.ReplayImporter; import itdelatrisu.opsu.ui.UI; +import itdelatrisu.opsu.ui.animations.AnimatedValue; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import java.io.File; import org.newdawn.slick.Color; 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.BasicGameState; @@ -47,6 +48,9 @@ * Loads game resources and enters "Main Menu" state. */ public class Splash extends BasicGameState { + /** Minimum time, in milliseconds, to display the splash screen (and fade in the logo). */ + private static final int MIN_SPLASH_TIME = 300; + /** Whether or not loading has completed. */ private boolean finished = false; @@ -59,6 +63,9 @@ public class Splash extends BasicGameState { /** Whether the skin being loaded is a new skin (for program restarts). */ private boolean newSkin = false; + /** Logo alpha level. */ + private AnimatedValue logoAlpha; + // game-related variables private int state; private GameContainer container; @@ -80,6 +87,8 @@ public void init(GameContainer container, StateBasedGame game) // load Utils class first (needed in other 'init' methods) Utils.init(container, game); + // fade in logo + this.logoAlpha = new AnimatedValue(MIN_SPLASH_TIME, 0f, 1f, AnimationEquation.LINEAR); GameImage.MENU_LOGO.getImage().setAlpha(0f); } @@ -144,13 +153,11 @@ public void run() { } // fade in logo - Image logo = GameImage.MENU_LOGO.getImage(); - float alpha = logo.getAlpha(); - if (alpha < 1f) - logo.setAlpha(alpha + (delta / 500f)); + if (logoAlpha.update(delta)) + GameImage.MENU_LOGO.getImage().setAlpha(logoAlpha.getValue()); // change states when loading complete - if (finished && alpha >= 1f) { + if (finished && logoAlpha.getValue() >= 1f) { // initialize song list if (BeatmapSetList.get().size() > 0) { BeatmapSetList.get().init(); From 940e9baa41674d5a272bfad2a55f53a86d518b51 Mon Sep 17 00:00:00 2001 From: MatteoS Date: Sat, 8 Aug 2015 14:43:56 +0200 Subject: [PATCH 11/34] Correct detection of GLSL version for curve rendering --- src/itdelatrisu/opsu/objects/curves/Curve.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 74ad3864..a22c4b29 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -87,7 +87,7 @@ public static void init(int width, int height, float circleSize, Color borderCol Curve.borderColor = borderColor; ContextCapabilities capabilities = GLContext.getCapabilities(); - mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL30; + mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL33; if (mmsliderSupported) CurveRenderState.init(width, height, circleSize); else { From 110e54e06349cc9502c32c344efcec333a97176e Mon Sep 17 00:00:00 2001 From: MatteoS Date: Sat, 8 Aug 2015 16:20:00 +0200 Subject: [PATCH 12/34] Fix crash on pressing extra mouse buttons --- src/org/newdawn/slick/Input.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/newdawn/slick/Input.java b/src/org/newdawn/slick/Input.java index 1066ca34..c74eff95 100644 --- a/src/org/newdawn/slick/Input.java +++ b/src/org/newdawn/slick/Input.java @@ -1233,7 +1233,7 @@ public void poll(int width, int height) { } while (Mouse.next()) { - if (Mouse.getEventButton() >= 0) { + if (Mouse.getEventButton() >= 0 && Mouse.getEventButton() < mousePressed.length) { if (Mouse.getEventButtonState()) { consumed = false; mousePressed[Mouse.getEventButton()] = true; From b537b83736b6afef9a9b2c8ca5bae99031afb6bd Mon Sep 17 00:00:00 2001 From: MatteoS Date: Sat, 8 Aug 2015 16:29:08 +0200 Subject: [PATCH 13/34] Game doesn't crash when being unable to go to the github repo page --- src/itdelatrisu/opsu/states/MainMenu.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 383557d3..50dfd3b4 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -501,7 +501,7 @@ else if (downloadsButton.contains(x, y)) { else if (repoButton != null && repoButton.contains(x, y)) { try { Desktop.getDesktop().browse(Options.REPOSITORY_URI); - } catch (IOException e) { + } catch (IOException | UnsupportedOperationException e) { ErrorHandler.error("Could not browse to repository URI.", e, false); } } From 40ab94794f7eb71c131a6200a2c0a0d87be849ab Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Sat, 8 Aug 2015 12:04:15 -0500 Subject: [PATCH 14/34] Bug fixes and tweaks. - Check if other Desktop actions are supported (follow-up to #114). - The cursor-middle image is no longer scaled (when clicking). - Changed the options menu background image (created with Trianglify at http://qrohlf.com/trianglify/), and made it fit the entire page. - Slightly increased minimum splash screen time. - Switched more animations to use AnimatedValue. Signed-off-by: Jeffrey Han --- res/options-background.jpg | Bin 116484 -> 0 bytes res/options-background.png | Bin 0 -> 348154 bytes src/itdelatrisu/opsu/ErrorHandler.java | 161 ++++++++++++------- src/itdelatrisu/opsu/states/MainMenu.java | 6 +- src/itdelatrisu/opsu/states/OptionsMenu.java | 10 +- src/itdelatrisu/opsu/states/Splash.java | 2 +- src/itdelatrisu/opsu/ui/Cursor.java | 2 - src/itdelatrisu/opsu/ui/UI.java | 24 +-- 8 files changed, 116 insertions(+), 89 deletions(-) delete mode 100644 res/options-background.jpg create mode 100644 res/options-background.png diff --git a/res/options-background.jpg b/res/options-background.jpg deleted file mode 100644 index d590ad6b847222ab1fe723f12d7249e67b6d66f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116484 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