-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Image HUD element. #5756
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Image HUD element. #5756
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| /* | ||
| * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). | ||
| * Copyright (c) Meteor Development. | ||
| */ | ||
|
|
||
| package meteordevelopment.meteorclient.systems.hud.elements; | ||
|
|
||
| import meteordevelopment.meteorclient.gui.renderer.packer.TextureRegion; | ||
| import meteordevelopment.meteorclient.settings.DoubleSetting; | ||
| import meteordevelopment.meteorclient.settings.Setting; | ||
| import meteordevelopment.meteorclient.settings.SettingGroup; | ||
| import meteordevelopment.meteorclient.settings.StringSetting; | ||
| import meteordevelopment.meteorclient.systems.hud.Hud; | ||
| import meteordevelopment.meteorclient.systems.hud.HudElement; | ||
| import meteordevelopment.meteorclient.systems.hud.HudElementInfo; | ||
| import meteordevelopment.meteorclient.systems.hud.HudRenderer; | ||
| import meteordevelopment.meteorclient.utils.misc.texture.ImageData; | ||
| import meteordevelopment.meteorclient.utils.misc.texture.ImageDataFactory; | ||
| import meteordevelopment.meteorclient.utils.misc.texture.TextureUtils; | ||
| import meteordevelopment.meteorclient.utils.render.color.Color; | ||
| import net.minecraft.util.Identifier; | ||
|
|
||
| import javax.imageio.ImageIO; | ||
| import javax.imageio.ImageReader; | ||
| import javax.imageio.stream.ImageInputStream; | ||
| import java.io.FileInputStream; | ||
| import java.io.InputStream; | ||
| import java.net.URL; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
|
|
||
| import static meteordevelopment.meteorclient.MeteorClient.*; | ||
| import static meteordevelopment.meteorclient.utils.misc.texture.TextureUtils.getCurrentAnimationFrame; | ||
| import static meteordevelopment.meteorclient.utils.misc.texture.TextureUtils.registerTexture; | ||
| import static org.lwjgl.opengl.GL11C.GL_MAX_TEXTURE_SIZE; | ||
| import static org.lwjgl.opengl.GL11C.glGetInteger; | ||
|
|
||
| public class ImageHud extends HudElement { | ||
| public static final HudElementInfo<ImageHud> INFO = new HudElementInfo<>(Hud.GROUP, "image", "Cures your ADHD.", ImageHud::new); | ||
| public static final int MAX_TEX_SIZE = glGetInteger(GL_MAX_TEXTURE_SIZE); | ||
| private static final Identifier DEFAULT_TEXTURE = Identifier.of(MOD_ID,"textures/icons/gui/default_image.png"); | ||
| private static final Identifier LOADING_TEXTURE = Identifier.of(MOD_ID,"textures/icons/gui/loading_image.png"); | ||
| private static final Color TRANSPARENT = new Color(255, 255, 255, 255); | ||
| private Identifier texture; | ||
| private final ExecutorService worker = Executors.newSingleThreadExecutor(); | ||
| private ImageData cachedImageData; | ||
| private CompletableFuture<Void> currentImageDataFuture; | ||
|
|
||
| private final SettingGroup sgGeneral = settings.getDefaultGroup(); | ||
|
|
||
| public ImageHud() { | ||
| super(INFO); | ||
| setSize(128,128); | ||
| } | ||
|
|
||
| private final Setting<String> path = sgGeneral.add(new StringSetting.Builder() | ||
| .name("Path") | ||
| .description("The full path / link of the image") | ||
| .wide() | ||
| .onChanged(this::composeImage) | ||
| .build() | ||
| ); | ||
|
|
||
| public final Setting<Double> scale = sgGeneral.add(new DoubleSetting.Builder() | ||
| .name("scale") | ||
| .description("Custom scale.") | ||
| .defaultValue(1) | ||
| .min(0.1) | ||
| .sliderRange(0.5, 2) | ||
| .max(10) | ||
| .onChanged(sc -> { | ||
| if (cachedImageData != null) { | ||
| setSize(cachedImageData.width * sc, cachedImageData.height * sc); | ||
| } | ||
| }) | ||
| .build() | ||
| ); | ||
|
|
||
| /** | ||
| * Composes the image asynchronously, since it can be a very slow process for animated images or big ones. | ||
| * @param path the URI in String format. | ||
| */ | ||
| private void composeImage(String path) { | ||
| // Cancel the future immediately to recreate another texture for the user. | ||
| if (currentImageDataFuture != null && !currentImageDataFuture.isDone()) { | ||
| currentImageDataFuture.cancel(true); | ||
|
||
| } | ||
| // Parse URI | ||
| String parsed = path.replace("\"", "").replace("\\", "/"); | ||
| String name = parsed.substring(parsed.lastIndexOf("/") + 1); | ||
| cachedImageData = null; | ||
|
|
||
| currentImageDataFuture = CompletableFuture.supplyAsync(() -> { | ||
| try { | ||
| InputStream imageFile = path.startsWith("http") ? new URL(path).openStream() : new FileInputStream(parsed); | ||
|
||
| ImageInputStream stream = ImageIO.createImageInputStream(imageFile); | ||
| ImageReader reader = ImageIO.getImageReaders(stream).next(); | ||
| reader.setInput(stream); | ||
| if (reader.getFormatName().equals("gif")) { | ||
| return ImageDataFactory.fromGIF(name, reader); | ||
| } else { | ||
| return ImageDataFactory.fromStatic(name, reader); | ||
| } | ||
| } catch (Exception e) { | ||
| LOG.debug("Failed to load image", e); | ||
| texture = null; | ||
|
||
| return null; | ||
| } | ||
| }, worker).thenAcceptAsync(data -> { | ||
| if (data != null) { | ||
| if (texture != null) mc.getTextureManager().destroyTexture(texture); | ||
| texture = registerTexture(data); | ||
| setSize(data.width * scale.get(), data.height * scale.get()); | ||
| cachedImageData = data; | ||
| } | ||
| },mc); | ||
| } | ||
|
|
||
| @Override | ||
| public void render(HudRenderer renderer) { | ||
| if (currentImageDataFuture != null && !currentImageDataFuture.isDone()) { | ||
| renderer.texture(LOADING_TEXTURE, getX(), getY(), 128, 128, TRANSPARENT, scale.get().floatValue()); | ||
| } | ||
| else if (cachedImageData == null || texture == null) { | ||
| renderer.texture(DEFAULT_TEXTURE,getX(),getY(),128,128,TRANSPARENT,scale.get().floatValue()); | ||
| } | ||
| else { | ||
| try { | ||
| if (cachedImageData.delays.isEmpty()){ | ||
| renderer.texture(texture, getX(), getY(), cachedImageData.width, cachedImageData.height, TRANSPARENT, scale.get().floatValue()); | ||
| } else { | ||
| renderGif(renderer, cachedImageData, texture, x, y, scale.get().floatValue()); | ||
| } | ||
| } catch (Exception e) { | ||
| LOG.debug("Failed to render image", e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static void renderGif(HudRenderer renderer, ImageData imageData, Identifier texture, int x, int y, float scale) { | ||
| int frameIndex = getCurrentAnimationFrame(imageData.delays); | ||
| int row = frameIndex % imageData.framesPerColumn; | ||
| int column = frameIndex / imageData.framesPerColumn; | ||
| TextureRegion textureRegion = new TextureRegion(imageData.width,imageData.height); | ||
|
|
||
| textureRegion.x1 = (float) (column * imageData.width) / imageData.canvasWidth; | ||
| textureRegion.y1 = (float) (row * imageData.height) / imageData.canvasHeight; | ||
| textureRegion.x2 = (float) ((column + 1) * imageData.width) / imageData.canvasWidth; | ||
| textureRegion.y2 = (float) ((row + 1) * imageData.height) / imageData.canvasHeight; | ||
|
|
||
| renderer.texture(texture,x,y,imageData.width,imageData.height,textureRegion,TRANSPARENT,scale); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /* | ||
| * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). | ||
| * Copyright (c) Meteor Development. | ||
| */ | ||
|
|
||
| package meteordevelopment.meteorclient.utils.misc.texture; | ||
|
|
||
| import java.awt.*; | ||
|
|
||
| public record FrameMetadata(int delay, String disposal, Offset offset, Color backgroundColor) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /* | ||
| * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). | ||
| * Copyright (c) Meteor Development. | ||
| */ | ||
|
|
||
| package meteordevelopment.meteorclient.utils.misc.texture; | ||
|
|
||
| import org.jetbrains.annotations.NotNull; | ||
| import org.w3c.dom.NodeList; | ||
|
|
||
| import javax.imageio.metadata.IIOMetadata; | ||
| import javax.imageio.metadata.IIOMetadataNode; | ||
| import java.awt.*; | ||
|
|
||
| public class FrameMetadataFactory { | ||
| /** | ||
| * Factory pattern to get the FrameMetadata object to compose a GIF. This factory can be extended in the future in case | ||
| * we want other animated formats like APNG or WEBP. (Not that Oracle's jdk supports them yet). | ||
| * @param metadata the GIF's IIOMetadata object from the image reader. | ||
| * It effectively contains all the metadata from a GIF frame. | ||
| * @return FrameMetadata. | ||
| */ | ||
| public static FrameMetadata fromGIF(IIOMetadata metadata) { | ||
| IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName()); | ||
| IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0); | ||
| IIOMetadataNode desc = (IIOMetadataNode) root.getElementsByTagName("ImageDescriptor").item(0); | ||
| int delay = Integer.parseInt(gce.getAttribute("delayTime")); | ||
| String disposal = gce.getAttribute("disposalMethod"); | ||
| int topPos = Integer.parseInt(desc.getAttribute("imageTopPosition")); | ||
| int leftPos = Integer.parseInt(desc.getAttribute("imageLeftPosition")); | ||
| if (disposal.equals("restoreToBackground")) return new FrameMetadata(delay, disposal, new Offset(leftPos,topPos), getBgColor(root)); | ||
| return new FrameMetadata(delay, disposal, new Offset(leftPos,topPos), new Color(0,0,0)); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the background color from a GIF's metadata. Needs to locate the index of the color in the LogicalScreenDescriptor | ||
| * and search it inside the ColorTableEntry list in the GlobalColorTable. | ||
| * @param root a IIOMetadataNode object (root) from the IIOMetadata. | ||
| * @return Color. | ||
| */ | ||
| private static java.awt.Color getBgColor(IIOMetadataNode root) { | ||
| IIOMetadataNode lsd = (IIOMetadataNode) root.getElementsByTagName("LogicalScreenDescriptor").item(0); // LOL | ||
| IIOMetadataNode gct = (IIOMetadataNode) root.getElementsByTagName("GlobalColorTable").item(0); | ||
| int bgcIndex = Integer.parseInt(lsd.getAttribute("backgroundColorIndex")); | ||
| return getBgColor(gct, bgcIndex); | ||
| } | ||
|
|
||
| private static @NotNull java.awt.Color getBgColor(IIOMetadataNode gct, int bgcIndex) { | ||
| java.awt.Color bgColor = new java.awt.Color(0,0,0); | ||
| if (gct != null) { | ||
| NodeList colorEntries = gct.getElementsByTagName("ColorTableEntry"); | ||
| for (int i = 0; i < colorEntries.getLength(); i++) { | ||
| IIOMetadataNode colorEntry = (IIOMetadataNode) colorEntries.item(i); | ||
| int colorIndex = Integer.parseInt(colorEntry.getAttribute("index")); | ||
| if (colorIndex == bgcIndex) { | ||
| bgColor = new java.awt.Color(Integer.parseInt(colorEntry.getAttribute("red")), | ||
| Integer.parseInt(colorEntry.getAttribute("green")), | ||
| Integer.parseInt(colorEntry.getAttribute("blue"))); | ||
| } | ||
| } | ||
| } | ||
| return bgColor; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| /* | ||
| * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). | ||
| * Copyright (c) Meteor Development. | ||
| */ | ||
|
|
||
| package meteordevelopment.meteorclient.utils.misc.texture; | ||
|
|
||
| import net.minecraft.client.texture.NativeImage; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class ImageData { | ||
| String name; | ||
| NativeImage texture; | ||
| public int width; | ||
| public int height; | ||
| public int canvasWidth; | ||
| public int canvasHeight; | ||
| public int framesPerColumn; | ||
| int totalFrames; | ||
| public List<Integer> delays; | ||
|
||
|
|
||
| public ImageData(String name) { | ||
| this.name = name; | ||
| } | ||
|
|
||
| public int getColumns(){ | ||
| return (int) Math.ceil(totalFrames / (double) framesPerColumn); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be
Util.getIoWorkerExecutor()or even virtual thread