aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java128
-rw-r--r--test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java2
2 files changed, 95 insertions, 35 deletions
diff --git a/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java b/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java
index 5a48f5f..7389232 100644
--- a/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java
+++ b/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java
@@ -4,6 +4,9 @@ import com.mojang.blaze3d.Blaze3D;
import com.mojang.blaze3d.platform.NativeImage;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import dev.isxander.yacl.impl.utils.YACLConstants;
+import net.minecraft.CrashReport;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.ReportedException;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.texture.DynamicTexture;
@@ -17,6 +20,7 @@ import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
+import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.IOException;
@@ -151,18 +155,18 @@ public interface ImageRenderer {
private int currentFrame;
private double lastFrameTime;
- private final double frameDelay;
+ private final double[] frameDelays;
private final int frameCount;
private final int packCols, packRows;
private final int frameWidth, frameHeight;
- public AnimatedNativeImageBacked(NativeImage image, int frameWidth, int frameHeight, int frameCount, double frameDelayMS, int packCols, int packRows, ResourceLocation uniqueLocation) {
+ public AnimatedNativeImageBacked(NativeImage image, int frameWidth, int frameHeight, int frameCount, double[] frameDelayMS, int packCols, int packRows, ResourceLocation uniqueLocation) {
super(image, uniqueLocation);
this.frameWidth = frameWidth;
this.frameHeight = frameHeight;
this.frameCount = frameCount;
- this.frameDelay = frameDelayMS;
+ this.frameDelays = frameDelayMS;
this.packCols = packCols;
this.packRows = packRows;
}
@@ -186,15 +190,24 @@ public interface ImageRenderer {
ImageReader reader = ImageIO.getImageReadersBySuffix("gif").next();
reader.setInput(ImageIO.createImageInputStream(is));
- IIOMetadata metadata = reader.getImageMetadata(0);
- String metaFormatName = metadata.getNativeMetadataFormatName();
- IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName);
- IIOMetadataNode graphicsControlExtensionNode = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
- int delay = Integer.parseInt(graphicsControlExtensionNode.getAttribute("delayTime")) * 10;
- return createFromImageReader(reader, delay, uniqueLocation);
- } catch (IOException e) {
- throw new RuntimeException(e);
+
+ AnimFrameProvider animFrameFunction = i -> {
+ IIOMetadata metadata = reader.getImageMetadata(i);
+ String metaFormatName = metadata.getNativeMetadataFormatName();
+ IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName);
+ IIOMetadataNode graphicsControlExtensionNode = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
+ int delay = Integer.parseInt(graphicsControlExtensionNode.getAttribute("delayTime")) * 10;
+
+ return new AnimFrame(delay, 0, 0);
+ };
+
+ return createFromImageReader(reader, animFrameFunction, uniqueLocation);
+ } catch (Exception e) {
+ CrashReport crashReport = CrashReport.forThrowable(e, "Failed to load GIF image");
+ CrashReportCategory category = crashReport.addCategory("YACL Gui");
+ category.setDetail("Image identifier", uniqueLocation.toString());
+ throw new ReportedException(crashReport);
}
}
@@ -203,36 +216,43 @@ public interface ImageRenderer {
ImageReader reader = new WebPImageReaderSpi().createReaderInstance();
reader.setInput(ImageIO.createImageInputStream(is));
- int frameDelayMS = 0;
int numImages = reader.getNumImages(true); // Force reading of all frames
+ AnimFrameProvider animFrameFunction = i -> null;
if (numImages > 1) {
// WebP reader does not expose frame delay, prepare for reflection hell
- try {
- Class<?> webpReaderClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.WebPImageReader");
- Field framesField = webpReaderClass.getDeclaredField("frames");
- framesField.setAccessible(true);
- List<?> frames = (List<?>) framesField.get(reader);
- Object firstFrame = frames.get(0);
-
- Class<?> animationFrameClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.AnimationFrame");
- Field durationField = animationFrameClass.getDeclaredField("duration");
- durationField.setAccessible(true);
- frameDelayMS = (int) durationField.get(firstFrame);
- } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
- throw new RuntimeException(e);
- }
+ Class<?> webpReaderClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.WebPImageReader");
+ Field framesField = webpReaderClass.getDeclaredField("frames");
+ framesField.setAccessible(true);
+ List<?> frames = (List<?>) framesField.get(reader);
+
+ Class<?> animationFrameClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.AnimationFrame");
+ Field durationField = animationFrameClass.getDeclaredField("duration");
+ durationField.setAccessible(true);
+ Field boundsField = animationFrameClass.getDeclaredField("bounds");
+ boundsField.setAccessible(true);
+
+ animFrameFunction = i -> {
+ Rectangle bounds = (Rectangle) boundsField.get(frames.get(i));
+ return new AnimFrame((int) durationField.get(frames.get(i)), bounds.x, bounds.y);
+ };
// that was fun
}
- return createFromImageReader(reader, frameDelayMS, uniqueLocation);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ return createFromImageReader(reader, animFrameFunction, uniqueLocation);
+ } catch (Throwable e) {
+ CrashReport crashReport = CrashReport.forThrowable(e, "Failed to load WEBP image");
+ CrashReportCategory category = crashReport.addCategory("YACL Gui");
+ category.setDetail("Image identifier", uniqueLocation.toString());
+ throw new ReportedException(crashReport);
}
}
- private static AnimatedNativeImageBacked createFromImageReader(ImageReader reader, int frameDelayMS, ResourceLocation uniqueLocation) throws IOException {
+ private static AnimatedNativeImageBacked createFromImageReader(ImageReader reader, AnimFrameProvider animationProvider, ResourceLocation uniqueLocation) throws Exception {
int frameCount = reader.getNumImages(true);
+ // Because this is being backed into a texture atlas, we need a maximum dimension
+ // so you can get the texture atlas size.
+ // Smaller frames are given black borders
int frameWidth = IntStream.range(reader.getMinIndex(), frameCount).map(i -> {
try {
return reader.getWidth(i);
@@ -256,15 +276,42 @@ public interface ImageRenderer {
int rows = (int)Math.ceil(frameCount / (double)cols);
NativeImage image = new NativeImage(frameWidth * cols, frameHeight * rows, true);
+
+ // Fill whole atlas with black, as each frame may have different dimensions
+ // that would cause borders of transparent pixels to appear around the frames
for (int x = 0; x < frameWidth * cols; x++) {
for (int y = 0; y < frameHeight * rows; y++) {
image.setPixelRGBA(x, y, 0xFF000000);
}
}
+
+ BufferedImage bi = null;
+ Graphics2D graphics = null;
+
+ // each frame may have a different delay
+ double[] frameDelays = new double[frameCount];
+
for (int i = reader.getMinIndex(); i < frameCount - 1; i++) {
- BufferedImage bi = reader.read(i);
+ AnimFrame frame = animationProvider.get(i);
+ if (frameCount > 1) // frame will be null if not animation
+ frameDelays[i] = frame.durationMS;
+
+ if (bi == null) {
+ // first frame...
+ bi = reader.read(i);
+ graphics = bi.createGraphics();
+ } else {
+ // WebP reader sometimes provides delta frames, (only the pixels that changed since the last frame)
+ // so instead of overwriting the image every frame, we draw delta frames on top of the previous frame
+ // to keep a complete image.
+ BufferedImage deltaFrame = reader.read(i);
+ graphics.drawImage(deltaFrame, frame.xOffset, frame.yOffset, null);
+ }
+
+ // Each frame may have different dimensions, so we need to center them.
int xOffset = (frameWidth - bi.getWidth()) / 2;
int yOffset = (frameHeight - bi.getHeight()) / 2;
+
for (int w = 0; w < bi.getWidth(); w++) {
for (int h = 0; h < bi.getHeight(); h++) {
int rgb = bi.getRGB(w, h);
@@ -283,9 +330,15 @@ public interface ImageRenderer {
}
}
}
+ // gives the texture to GL for rendering
+ // usually, you create a native image with NativeImage.create, which sets the pixels and
+ // runs this function itself. In this case, we need to do it manually.
image.upload(0, 0, 0, false);
- return new AnimatedNativeImageBacked(image, frameWidth, frameHeight, frameCount, frameDelayMS, cols, rows, uniqueLocation);
+ graphics.dispose();
+ reader.dispose();
+
+ return new AnimatedNativeImageBacked(image, frameWidth, frameHeight, frameCount, frameDelays, cols, rows, uniqueLocation);
}
@Override
@@ -313,14 +366,21 @@ public interface ImageRenderer {
if (frameCount > 1) {
double timeMS = Blaze3D.getTime() * 1000;
if (lastFrameTime == 0) lastFrameTime = timeMS;
- if (timeMS - lastFrameTime >= frameDelay) {
+ if (timeMS - lastFrameTime >= frameDelays[currentFrame]) {
currentFrame++;
lastFrameTime = timeMS;
}
- if (currentFrame >= frameCount - 1) currentFrame = 0;
+ if (currentFrame >= frameCount - 1)
+ currentFrame = 0;
}
return targetHeight;
}
+
+ @FunctionalInterface
+ private interface AnimFrameProvider {
+ AnimFrame get(int frame) throws Exception;
+ }
+ private record AnimFrame(int durationMS, int xOffset, int yOffset) {}
}
}
diff --git a/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java b/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java
index 2da0423..8647d16 100644
--- a/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java
+++ b/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java
@@ -70,7 +70,7 @@ public class GuiTest {
.append(Component.literal("e").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("e")))))
.withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://isxander.dev")))
)
- .gifImage(Path.of("D:\\Xander\\Downloads\\showcaseMain.gif"), new ResourceLocation("yacl", "e.webp"))
+ .webpImage(Path.of("D:\\Xander\\Code\\isXander\\Controlify\\src\\main\\resources\\assets\\controlify\\textures\\screenshots\\reach-around-placement.webp"), new ResourceLocation("yacl", "e.webp"))
.build())
.binding(
defaults.booleanToggle,