diff --git a/src/itdelatrisu/opsu/beatmap/Beatmap.java b/src/itdelatrisu/opsu/beatmap/Beatmap.java index bfe16128..346072f3 100644 --- a/src/itdelatrisu/opsu/beatmap/Beatmap.java +++ b/src/itdelatrisu/opsu/beatmap/Beatmap.java @@ -27,7 +27,6 @@ import java.util.Map; import org.newdawn.slick.Color; import org.newdawn.slick.Image; -import org.newdawn.slick.SlickException; import org.newdawn.slick.util.Log; /** @@ -39,20 +38,19 @@ public class Beatmap implements Comparable { /** Background image cache. */ @SuppressWarnings("serial") - private static final LRUCache bgImageCache = new LRUCache(10) { + private static final LRUCache bgImageCache = new LRUCache(10) { @Override - public void eldestRemoved(Map.Entry eldest) { - Image img = eldest.getValue(); - if (img != null && !img.isDestroyed()) { - try { - img.destroy(); // destroy the removed image - } catch (SlickException e) { - Log.warn(String.format("Failed to destroy image '%s'.", img.getResourceReference()), e); - } - } + public void eldestRemoved(Map.Entry eldest) { + if (eldest.getKey() == lastBG) + lastBG = null; + ImageLoader imageLoader = eldest.getValue(); + imageLoader.destroy(); } }; + /** The last background image loaded. */ + private static File lastBG; + /** * Clears the background image cache. *

@@ -276,49 +274,74 @@ public class Beatmap implements Comparable { } /** - * Draws the beatmap background. + * Loads the beatmap background image. + */ + public void loadBackground() { + if (bg == null || bgImageCache.containsKey(bg) || !bg.isFile()) + return; + + if (lastBG != null) { + ImageLoader lastImageLoader = bgImageCache.get(lastBG); + if (lastImageLoader != null && lastImageLoader.isLoading()) { + lastImageLoader.interrupt(); // only allow loading one image at a time + bgImageCache.remove(lastBG); + } + } + ImageLoader imageLoader = new ImageLoader(bg); + bgImageCache.put(bg, imageLoader); + imageLoader.load(true); + lastBG = bg; + } + + /** + * Returns whether the beatmap background image is currently loading. + * @return true if loading + */ + public boolean isBackgroundLoading() { + if (bg == null) + return false; + ImageLoader imageLoader = bgImageCache.get(bg); + return (imageLoader != null && imageLoader.isLoading()); + } + + /** + * Draws the beatmap background image. * @param width the container width * @param height the container height * @param alpha the alpha value * @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio * @return true if successful, false if any errors were produced */ - public boolean drawBG(int width, int height, float alpha, boolean stretch) { + public boolean drawBackground(int width, int height, float alpha, boolean stretch) { if (bg == null) return false; - try { - Image bgImage = bgImageCache.get(this); - if (bgImage == null) { - if (!bg.isFile()) - return false; - bgImage = new Image(bg.getAbsolutePath()); - bgImageCache.put(bg, bgImage); - } - int swidth = width; - int sheight = height; - if (!stretch) { - // fit image to screen - if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y - sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth()); - else - swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight()); - } else { - // fill screen while maintaining aspect ratio - if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y - swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight()); - else - sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth()); - } - bgImage = bgImage.getScaledCopy(swidth, sheight); - - bgImage.setAlpha(alpha); - bgImage.drawCentered(width / 2, height / 2); - } catch (Exception e) { - Log.warn(String.format("Failed to get background image '%s'.", bg), e); - bg = null; // don't try to load the file again until a restart + ImageLoader imageLoader = bgImageCache.get(bg); + if (imageLoader == null) return false; + + Image bgImage = imageLoader.getImage(); + if (bgImage == null) + return true; + + int swidth = width; + int sheight = height; + if (!stretch) { + // fit image to screen + if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y + sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth()); + else + swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight()); + } else { + // fill screen while maintaining aspect ratio + if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y + swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight()); + else + sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth()); } + bgImage = bgImage.getScaledCopy(swidth, sheight); + bgImage.setAlpha(alpha); + bgImage.drawCentered(width / 2, height / 2); return true; } diff --git a/src/itdelatrisu/opsu/beatmap/ImageLoader.java b/src/itdelatrisu/opsu/beatmap/ImageLoader.java new file mode 100644 index 00000000..a6c3dcf6 --- /dev/null +++ b/src/itdelatrisu/opsu/beatmap/ImageLoader.java @@ -0,0 +1,179 @@ +/* + * 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.beatmap; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.newdawn.slick.Image; +import org.newdawn.slick.SlickException; +import org.newdawn.slick.opengl.ImageData; +import org.newdawn.slick.opengl.ImageDataFactory; +import org.newdawn.slick.opengl.LoadableImageData; +import org.newdawn.slick.util.Log; + +/** + * Simple threaded image loader for a single image file. + */ +public class ImageLoader { + /** The image file. */ + private final File file; + + /** The loaded image. */ + private Image image; + + /** The image data. */ + private LoadedImageData data; + + /** The image loader thread. */ + private Thread loaderThread; + + /** ImageData wrapper, needed because {@code ImageIOImageData} doesn't implement {@code getImageBufferData()}. */ + private class LoadedImageData implements ImageData { + /** The image data implementation. */ + private final ImageData imageData; + + /** The stored image. */ + private final ByteBuffer buffer; + + /** + * Constructor. + * @param imageData the class holding the image properties + * @param buffer the stored image + */ + public LoadedImageData(ImageData imageData, ByteBuffer buffer) { + this.imageData = imageData; + this.buffer = buffer; + } + + @Override public int getDepth() { return imageData.getDepth(); } + @Override public int getWidth() { return imageData.getWidth(); } + @Override public int getHeight() { return imageData.getHeight();} + @Override public int getTexWidth() { return imageData.getTexWidth(); } + @Override public int getTexHeight() { return imageData.getTexHeight(); } + @Override public ByteBuffer getImageBufferData() { return buffer; } + } + + /** Image loading thread. */ + private class ImageLoaderThread extends Thread { + /** The image file input stream. */ + private BufferedInputStream in; + + @Override + public void interrupt() { + super.interrupt(); + if (in != null) { + try { + in.close(); // interrupt I/O + } catch (IOException e) {} + } + } + + @Override + public void run() { + // load image data into a ByteBuffer to use constructor Image(ImageData) + LoadableImageData imageData = ImageDataFactory.getImageDataFor(file.getAbsolutePath()); + try (BufferedInputStream in = this.in = new BufferedInputStream(new FileInputStream(file))) { + ByteBuffer textureBuffer = imageData.loadImage(in, false, null); + if (!isInterrupted()) + data = new LoadedImageData(imageData, textureBuffer); + } catch (IOException e) { + if (!isInterrupted()) + Log.warn(String.format("Failed to load background image '%s'.", file), e); + } + this.in = null; + } + } + + /** + * Constructor. Call {@link ImageLoader#load(boolean)} to load the image. + * @param file the image file + */ + public ImageLoader(File file) { + this.file = file; + } + + /** + * Loads the image. + * @param threaded true to load the image data in a new thread + */ + public void load(boolean threaded) { + if (!file.isFile()) + return; + + if (threaded) { + if (loaderThread != null && loaderThread.isAlive()) + loaderThread.interrupt(); + loaderThread = new ImageLoaderThread(); + loaderThread.start(); + } else { + try { + image = new Image(file.getAbsolutePath()); + } catch (SlickException e) { + Log.warn(String.format("Failed to load background image '%s'.", file), e); + } + } + } + + /** + * Returns the image. + * @return the loaded image, or null if not loaded + */ + public Image getImage() { + if (image == null && data != null) { + image = new Image(data); + data = null; + } + return image; + } + + /** + * Returns whether an image is currently loading in another thread. + * @return true if loading, false otherwise + */ + public boolean isLoading() { return (loaderThread != null && loaderThread.isAlive()); } + + /** + * Interrupts the image loader, if running. + */ + public void interrupt() { + if (isLoading()) + loaderThread.interrupt(); + } + + /** + * Releases all resources. + */ + public void destroy() { + interrupt(); + loaderThread = null; + if (image != null && !image.isDestroyed()) { + try { + image.destroy(); + } catch (SlickException e) { + Log.warn(String.format("Failed to destroy image '%s'.", image.getResourceReference()), e); + } + image = null; + } + data = null; + } +} diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 5c006c98..f4cff494 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -305,7 +305,7 @@ public class Game extends BasicGameState { else dimLevel = 1f; } - if (Options.isDefaultPlayfieldForced() || !beatmap.drawBG(width, height, dimLevel, false)) { + if (Options.isDefaultPlayfieldForced() || !beatmap.drawBackground(width, height, dimLevel, false)) { Image playfield = GameImage.PLAYFIELD.getImage(); playfield.setAlpha(dimLevel); playfield.draw(); diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index 8a9c9d18..925f7f48 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -106,7 +106,7 @@ public class GameRanking extends BasicGameState { Beatmap beatmap = MusicController.getBeatmap(); // background - if (!beatmap.drawBG(width, height, 0.7f, true)) + if (!beatmap.drawBackground(width, height, 0.7f, true)) GameImage.PLAYFIELD.getImage().draw(0,0); // ranking screen elements diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 6b86cdcf..f0901df8 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -234,7 +234,7 @@ public class MainMenu extends BasicGameState { // draw background Beatmap beatmap = MusicController.getBeatmap(); if (Options.isDynamicBackgroundEnabled() && - beatmap != null && beatmap.drawBG(width, height, bgAlpha.getValue(), true)) + beatmap != null && beatmap.drawBackground(width, height, bgAlpha.getValue(), true)) ; else { Image bg = GameImage.MENU_BG.getImage(); @@ -349,7 +349,9 @@ public class MainMenu extends BasicGameState { MusicController.toggleTrackDimmed(0.33f); // fade in background - bgAlpha.update(delta); + Beatmap beatmap = MusicController.getBeatmap(); + if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading())) + bgAlpha.update(delta); // buttons int centerX = container.getWidth() / 2; diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index 251a50e4..498cba0f 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -216,6 +216,12 @@ public class SongMenu extends BasicGameState { /** Whether the song folder changed (notified via the watch service). */ private boolean songFolderChanged = false; + /** The last background image. */ + private File lastBackgroundImage; + + /** Background alpha level (for fade-in effect). */ + private AnimatedValue bgAlpha = new AnimatedValue(800, 0f, 1f, AnimationEquation.OUT_QUAD); + /** * Beatmaps whose difficulties were recently computed (if flag is non-null). * Unless the Boolean flag is null, then upon removal, the beatmap's objects will @@ -344,7 +350,7 @@ public class SongMenu extends BasicGameState { // background if (focusNode != null) { Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex); - if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true)) + if (!focusNodeBeatmap.drawBackground(width, height, bgAlpha.getValue(), true)) GameImage.PLAYFIELD.getImage().draw(); } @@ -545,6 +551,13 @@ public class SongMenu extends BasicGameState { } } + // fade in background + if (focusNode != null) { + Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex); + if (!focusNodeBeatmap.isBackgroundLoading()) + bgAlpha.update(delta); + } + // search search.setFocus(true); searchTimer += delta; @@ -996,6 +1009,7 @@ public class SongMenu extends BasicGameState { beatmapMenuTimer = -1; searchTransitionTimer = SEARCH_TRANSITION_TIME; songInfo = null; + bgAlpha.setTime(bgAlpha.getDuration()); // reset song stack randomStack = new Stack(); @@ -1253,6 +1267,14 @@ public class SongMenu extends BasicGameState { if (startNode.index == focusNode.index && startNode.beatmapIndex == -1) changeIndex(1); + // load background image + beatmap.loadBackground(); + boolean isBgNull = lastBackgroundImage == null || beatmap.bg == null; + if ((isBgNull && lastBackgroundImage != beatmap.bg) || (!isBgNull && !beatmap.bg.equals(lastBackgroundImage))) { + bgAlpha.setTime(0); + lastBackgroundImage = beatmap.bg; + } + return oldFocus; } @@ -1352,6 +1374,7 @@ public class SongMenu extends BasicGameState { searchTimer = SEARCH_DELAY; searchTransitionTimer = SEARCH_TRANSITION_TIME; searchResultString = null; + lastBackgroundImage = null; // reload songs in new thread reloadThread = new Thread() { diff --git a/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java index d1a7fcdf..4cad5959 100644 --- a/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java +++ b/src/itdelatrisu/opsu/ui/animations/AnimatedValue.java @@ -77,6 +77,11 @@ public class AnimatedValue { updateValue(); } + /** + * Returns the total animation duration, in milliseconds. + */ + public int getDuration() { return duration; } + /** * Sets the animation duration. * @param duration the new duration, in milliseconds