diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 3181abd4f9..d2e3a0b58a 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -719,7 +719,8 @@ public long getUniqueId() { } /** - * @return A shallow clone of this image. The data is not cloned. + * @see #deepClone() Deep clone. + * @return a shallow clone of this image. The data is not cloned. */ @Override public Image clone(){ @@ -731,6 +732,27 @@ public Image clone(){ return clone; } + /** + * @see #clone() Shallow clone. + * @return a deep clone of this image. The data is cloned. + */ + public Image deepClone() { + Image clone = (Image) super.clone(); + clone.mipMapSizes = mipMapSizes != null ? mipMapSizes.clone() : null; + clone.data = data != null ? dataDeepClone() : null; + clone.lastTextureState = new LastTextureState(); + clone.setUpdateNeeded(); + return clone; + } + + private ArrayList dataDeepClone() { + ArrayList clone = new ArrayList(data.size()); + for (ByteBuffer originalBuffer : data) { + clone.add(BufferUtils.createByteBuffer(originalBuffer, originalBuffer.capacity())); + } + return clone; + } + /** * Constructor instantiates a new Image object. All values * are undefined. diff --git a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg index b350b696f6..7d339204ec 100644 --- a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg +++ b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg @@ -1,4 +1,3 @@ INCLUDE com/jme3/asset/General.cfg # Desktop-specific loaders -LOADER com.jme3.cursors.plugins.CursorLoader : ani, cur, ico diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java new file mode 100644 index 0000000000..c630f7d1f7 --- /dev/null +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * 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 'jMonkeyEngine' nor the names of its 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. + */ +package com.jme3.cursors.plugins; + +import java.nio.IntBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.jme3.math.ColorRGBA; +import com.jme3.texture.Image; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ImageRaster; +import com.jme3.util.BufferUtils; + +/** + * Convert any image like object to a {@link JmeCursor}. + */ +public class CursorConverter { + /** + * Convert a {@link Texture2D} to a {@link JmeCursor}. + * + * The coordinate system used is the same specified in {@link JmeCursor}, the start point is 0, 0 being + * lower left. + * + * The cursor's hot spot will be the top left of the texture's image. + * + * {@param cursorTexture the texture to convert. No modifications will be applied.} + * {@return the {@link JmeCursor} object that contains the data for a cursor.} + */ + public static JmeCursor fromTexture(Texture2D cursorTexture) { + return fromTexture(cursorTexture, 0, cursorTexture.getImage().getHeight()); + } + + /** + * Convert a {@link Texture2D} to a {@link JmeCursor}. + * + * The coordinate system used is the same specified in {@link JmeCursor}, the start point is 0, 0 being + * lower left. + * + * {@param cursorTexture the texture to convert. No modifications will be applied.} + * {@param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} + * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} + * {@return the {@link JmeCursor} object that contains the data for a cursor.} + */ + public static JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot) { + Image image = cursorTexture.getImage(); + + int imageHeight = image.getHeight(); + int imageWidth = image.getWidth(); + + if (!hotspotInsideImageLimits(xHotspot, yHotspot, imageWidth, imageHeight)) { + throw new IllegalArgumentException( + "Cursor's hot spot must be inside image limits (width and height)" + ); + } + + IntBuffer adaptedImageData = getDataAsIntBuffer(image); + + JmeCursor jmeCursor = new JmeCursor(); + jmeCursor.setWidth(imageWidth); + jmeCursor.setHeight(imageHeight); + jmeCursor.setxHotSpot(xHotspot); + jmeCursor.setyHotSpot(yHotspot); + jmeCursor.setNumImages(1); + jmeCursor.setImagesDelay(null); + jmeCursor.setImagesData(adaptedImageData); + return jmeCursor; + } + + /** + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * interpreting each {@link Texture2D} object as a frame of the animated cursor. + * + * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being + * lower left. + * + * The cursor's hot spot will be the top left of the texture's image. + * + * {@param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied.} + * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} + * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + */ + public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDelay) { + int yHotspot = 0; + if (cursorFrames.length > 0) { + yHotspot = cursorFrames[0].getImage().getHeight(); + } + return fromTextureFrames(cursorFrames, frameDelay, 0, yHotspot); + } + + /** + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * interpreting each {@link Texture2D} object as a frame of the animated cursor. + * + * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being + * lower left. + * + * The cursor's hot spot will be the top left of the texture's image. + * + * {@param cursorFrames the frames that will make up the cursor animation. No modifications will be applied.} + * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} + * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + */ + public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frameDelays) { + int yHotspot = 0; + if (cursorFrames.length > 0) { + yHotspot = cursorFrames[0].getImage().getHeight(); + } + return fromTextureFrames(cursorFrames, frameDelays, 0, yHotspot); + } + + /** + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * interpreting each {@link Texture2D} object as a frame of the animated cursor. + * + * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being + * lower left. + * + * {@param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied.} + * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} + * {@param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} + * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} + * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + */ + public static JmeCursor fromTextureFrames( + Texture2D[] cursorFrames, + int frameDelay, + int xHotspot, + int yHotspot) { + int[] frameRates = new int[cursorFrames.length]; + Arrays.fill(frameRates, frameDelay); + return fromTextureFrames(cursorFrames, frameRates, xHotspot, yHotspot); + } + + /** + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * interpreting each {@link Texture2D} object as a frame of the animated cursor. + * + * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being + * lower left. + * + * {@param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied.} + * {@param frameDelays the time delay that will take each frame to change to the next frame. Because of it, + * it must contains as many delays as frames (lengths of cursorFrames and frameDelays + * arrays must be equal).} + * {@param xHotspot tame specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} + * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} + * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + */ + public static JmeCursor fromTextureFrames( + Texture2D[] cursorFrames, + int[] frameDelays, + int xHotspot, + int yHotspot) { + if (frameDelays.length != cursorFrames.length) { + throw new IllegalArgumentException( + "The lengths of cursorFrames and frameDelays arrays must be equal" + ); + } + + List imageFrames = Arrays.stream(cursorFrames) + //Avoid working and accidentally modifying original values + .map((frame) -> frame.getImage()) + .collect(Collectors.toList()); + + List imageFrameHeights = imageFrames + .stream() + .map((image) -> image.getHeight()) + .distinct() + .collect(Collectors.toList()); + + List imageFrameWidths = imageFrames + .stream() + .map((image) -> image.getWidth()) + .distinct() + .collect(Collectors.toList()); + + if (imageFrameHeights.size() > 1 || imageFrameWidths.size() > 1) { + throw new IllegalArgumentException("Some images from the Texture2D objects have different sizes"); + } + + int imageHeight = imageFrameHeights.get(0); + int imageWidth = imageFrameWidths.get(0); + + if (!hotspotInsideImageLimits(xHotspot, yHotspot, imageWidth, imageHeight)) { + throw new IllegalArgumentException( + "Cursor's hot spot must be inside image limits (width and height)" + ); + } + + IntBuffer imagesData = BufferUtils.createIntBuffer(imageHeight * imageWidth * cursorFrames.length); + + List framesData = imageFrames + .stream() + .map((image) -> getDataAsIntBuffer(image)) + .collect(Collectors.toList()); + + for (IntBuffer frameData : framesData) { + imagesData.put(frameData); + } + imagesData.flip(); + + JmeCursor jmeCursor = new JmeCursor(); + jmeCursor.setWidth(imageWidth); + jmeCursor.setHeight(imageHeight); + jmeCursor.setxHotSpot(xHotspot); + jmeCursor.setyHotSpot(yHotspot); + jmeCursor.setNumImages(cursorFrames.length); + jmeCursor.setImagesDelay(BufferUtils.createIntBuffer(frameDelays)); + jmeCursor.setImagesData(imagesData); + return jmeCursor; + } + + private static IntBuffer getDataAsIntBuffer(Image image) { + int width = image.getWidth(); + int height = image.getHeight(); + + ImageRaster raster = ImageRaster.create(image); + + IntBuffer data = BufferUtils.createIntBuffer(width * height); + + //ARGB color system is needed to show cursors correctly. + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + ColorRGBA color = raster.getPixel(x, y); + + int a = (int) (color.a * 255) & 0xFF; + int r = (int) (color.r * 255) & 0xFF; + int g = (int) (color.g * 255) & 0xFF; + int b = (int) (color.b * 255) & 0xFF; + + int argb = (a << 24) | (r << 16) | (g << 8) | b; + + data.put(argb); + } + } + + data.flip(); + return data; + } + + private static boolean hotspotInsideImageLimits( + int xHotspot, + int yHotspot, + int imageWidth, + int imageHeight) { + return xHotspot >= 0 && xHotspot <= imageWidth && yHotspot >= 0 && yHotspot <= imageHeight; + } +} diff --git a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java index 7d530ad960..b60fa7c1a7 100644 --- a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java @@ -51,9 +51,15 @@ import javax.imageio.ImageIO; /** + * Supports loading of .ico, .ani, and .cur cursor file formats. + * * Created Jun 5, 2012 9:45:58 AM * @author MadJack + * + * @deprecated This class is not cross-platform, and the supported file formats are no longer commonly used. + * Use {@link com.jme3.cursors.plugins.CursorConverter} instead. */ +@Deprecated public class CursorLoader implements AssetLoader { final private static int FDE_OFFSET = 6; // first directory entry offset diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index 3ba14216d7..84614be13f 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -1,11 +1,21 @@ package jme3test.gui; import com.jme3.app.SimpleApplication; +import com.jme3.cursors.plugins.CursorConverter; import com.jme3.cursors.plugins.JmeCursor; +import com.jme3.texture.Image; +import com.jme3.texture.image.ImageRaster; +import com.jme3.texture.Texture2D; +import com.jme3.math.ColorRGBA; + import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Collectors; /** * This test class demonstrate how to change cursor in jME3. + * + * The different methods of {@link com.jme3.cursors.plugins.CursorConverter} will be tested. * * NOTE: This will not work on Android as it does not support cursors. * @@ -34,34 +44,99 @@ public void simpleInitApp() { /* * To make jME3 use a custom cursor it is as simple as putting the - * .cur/.ico/.ani file in an asset directory. Here we use + * image file in an asset directory. Here we use * "Textures/GUI/Cursors". * - * For the purpose of this demonstration we load 3 different cursors and add them - * into an array list and switch cursor every 8 seconds. + * For the purpose of this demonstration we load 3 different cursors and + * switch cursor every 5 seconds, applying differents configurations with + * {@link com.jme3.cursors.plugins.CursorConverter}. * - * The first ico has been made by Sirea and the set can be found here: + * At date of 2026/06/01: + * + * The nyan cat cursor has been made by Sirea. Under the Attribution Required (CC by) license: * http://www.rw-designer.com/icon-set/nyan-cat * - * The second cursor has been made by Virum64 and is Public Domain. + * The meme face cursor has been made by Virum64. Released to Public Domain. * http://www.rw-designer.com/cursor-set/memes-faces-v64 * - * The animated cursor has been made by Pointer Adic and can be found here: + * The animated monkey cursor has been made by Pointer Adic. Released to Public Domain: * http://www.rw-designer.com/cursor-set/monkey + * + * The three cursor examples have been converted to png format. + * Checking and following the license restrictions in the process. + */ + + Image memeImage = ((Image) assetManager.loadAsset("Textures/Cursors/meme.png")).deepClone(); + flipVertically(memeImage); + Texture2D memeTexture = new Texture2D(memeImage); + + Image nyancatImage = ((Image) assetManager.loadAsset("Textures/Cursors/nyancat.png")).deepClone(); + flipVertically(nyancatImage); + Texture2D nyancatTexture = new Texture2D(nyancatImage); + + //Shows a cursor with default hot spot. + cursors.add(CursorConverter.fromTexture(memeTexture)); + + //Shows a cursor with custom hot spot. + cursors.add(CursorConverter.fromTexture(nyancatTexture, 10, 10)); + + /* + * For animated cursors. Each frame must be loaded. + * + * Differents configuration with the monkey animated cursor will be + * showed by changing the frame delays. */ - cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/meme.cur")); - cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/nyancat.ico")); - cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/monkey.ani")); + String[] monkeyFramePaths = { + "Textures/Cursors/monkey/frame_0001.png", + "Textures/Cursors/monkey/frame_0002.png", + "Textures/Cursors/monkey/frame_0003.png", + "Textures/Cursors/monkey/frame_0004.png", + "Textures/Cursors/monkey/frame_0005.png", + "Textures/Cursors/monkey/frame_0006.png" + }; + + Texture2D[] monkeyFrames = Arrays.stream(monkeyFramePaths) + .map(framePath -> ((Image) assetManager.loadAsset(framePath)).deepClone()) + .peek(frameImage -> flipVertically(frameImage)) + .map(frameImage -> new Texture2D(frameImage)) + .toArray(Texture2D[]::new); + + //Shows monkey cursor with same frame delay for each frame and default hot spot. + cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, 60)); + + //Shows monkey cursor with custom frame delay for each frame and default hot spot. + cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 40, 40, 200, 200, 200})); + + //Shows monkey cursor with same frame delay for each frame and custom hot spot. + cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, 150, 5, 7)); + + //Shows monkey cursor with custom frame delay for each frame and hot spot. + cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 200, 40, 200, 40, 200}, 10, 20)); sysTime = System.currentTimeMillis(); inputManager.setMouseCursor(cursors.get(count)); } + private void flipVertically(Image image) { + int height = image.getHeight(); + int width = image.getWidth(); + + ImageRaster raster = ImageRaster.create(image); + + for (int i = 0; i < width; i++) { + for (int j = 0; j < height / 2; j++){ + ColorRGBA reserve = raster.getPixel(i, j); + raster.setPixel(i, j, raster.getPixel(i, height - j -1)); + raster.setPixel(i, height - j -1, reserve); + } + } + } + @Override public void simpleUpdate(float tpf) { long currentTime = System.currentTimeMillis(); - if (currentTime - sysTime > 8000) { + if (currentTime - sysTime > 5000) { count++; if (count >= cursors.size()) { count = 0; diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/meme.cur b/jme3-testdata/src/main/resources/Textures/Cursors/meme.cur deleted file mode 100644 index 89c8a68e79..0000000000 Binary files a/jme3-testdata/src/main/resources/Textures/Cursors/meme.cur and /dev/null differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/meme.png b/jme3-testdata/src/main/resources/Textures/Cursors/meme.png new file mode 100644 index 0000000000..4be43cc96b Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/meme.png differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey.ani b/jme3-testdata/src/main/resources/Textures/Cursors/monkey.ani deleted file mode 100644 index 68b125ce21..0000000000 Binary files a/jme3-testdata/src/main/resources/Textures/Cursors/monkey.ani and /dev/null differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0001.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0001.png new file mode 100644 index 0000000000..c9c1cadf2d Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0001.png differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png new file mode 100644 index 0000000000..790e325001 Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png new file mode 100644 index 0000000000..8bf8c1b8a4 Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0004.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0004.png new file mode 100644 index 0000000000..9aba470550 Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0004.png differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png new file mode 100644 index 0000000000..f39c59cde5 Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0006.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0006.png new file mode 100644 index 0000000000..a9b2ac3778 Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0006.png differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/monkey.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/monkey.png new file mode 100644 index 0000000000..fdb5a0e994 Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/monkey.png differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.ico b/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.ico deleted file mode 100644 index ab26e914cb..0000000000 Binary files a/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.ico and /dev/null differ diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.png b/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.png new file mode 100644 index 0000000000..36e9b33938 Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.png differ