diff --git a/src/main/java/meteordevelopment/meteorclient/systems/hud/Hud.java b/src/main/java/meteordevelopment/meteorclient/systems/hud/Hud.java index 95ccdec719..f7a31f76b1 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/hud/Hud.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/hud/Hud.java @@ -129,6 +129,7 @@ public void init() { register(ModuleInfosHud.INFO); register(PotionTimersHud.INFO); register(CombatHud.INFO); + register(ImageHud.INFO); // Default config if (isFirstInit) resetToDefaultElements(); diff --git a/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java b/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java index cd0b41be09..9b451e014f 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java @@ -12,6 +12,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import meteordevelopment.meteorclient.MeteorClient; import meteordevelopment.meteorclient.events.meteor.CustomFontChangedEvent; +import meteordevelopment.meteorclient.gui.renderer.packer.TextureRegion; import meteordevelopment.meteorclient.renderer.*; import meteordevelopment.meteorclient.renderer.text.CustomTextRenderer; import meteordevelopment.meteorclient.renderer.text.Font; @@ -135,6 +136,18 @@ public void texture(Identifier id, double x, double y, double width, double heig Renderer2D.TEXTURE.render(mc.getTextureManager().getTexture(id).getGlTextureView()); } + public void texture(Identifier id, double x, double y, double width, double height, TextureRegion textureRegion, Color color, float scale) { + Renderer2D.TEXTURE.begin(); + Renderer2D.TEXTURE.texQuad(x, y, width * scale, height * scale, textureRegion, color); + Renderer2D.TEXTURE.render(mc.getTextureManager().getTexture(id).getGlTextureView()); + } + + public void texture(Identifier id, double x, double y, double width, double height, Color color, float scale) { + Renderer2D.TEXTURE.begin(); + Renderer2D.TEXTURE.texQuad(x, y, width * scale, height * scale, color); + Renderer2D.TEXTURE.render(mc.getTextureManager().getTexture(id).getGlTextureView()); + } + public double text(String text, double x, double y, Color color, boolean shadow, double scale) { if (scale == -1) scale = hud.getTextScale(); diff --git a/src/main/java/meteordevelopment/meteorclient/systems/hud/elements/ImageHud.java b/src/main/java/meteordevelopment/meteorclient/systems/hud/elements/ImageHud.java new file mode 100644 index 0000000000..549cbd7937 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/systems/hud/elements/ImageHud.java @@ -0,0 +1,171 @@ +/* + * 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 net.minecraft.util.Util; +import net.minecraft.util.thread.NameableExecutor; + +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.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +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 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 static final int DEBOUNCE_TIME = 2; // 2 Seconds before rerunning. + private Identifier texture; + private final NameableExecutor worker = Util.getIoWorkerExecutor(); + private ImageData cachedImageData; + private CompletableFuture currentImageDataFuture; + private CompletableFuture debounceTask; + private long lastModified = System.currentTimeMillis(); + private String lastPath = ""; + + private final SettingGroup sgGeneral = settings.getDefaultGroup(); + + public ImageHud() { + super(INFO); + setSize(128,128); + } + + private final Setting path = sgGeneral.add(new StringSetting.Builder() + .name("Path") + .description("The full path / link of the image") + .wide() + .onChanged(path -> { + lastModified = System.currentTimeMillis(); + if (debounceTask != null && !debounceTask.isDone()) { + debounceTask.cancel(false); + } + debounceTask = CompletableFuture.runAsync(() -> { + if (System.currentTimeMillis() - lastModified > DEBOUNCE_TIME && !lastPath.equals(path)) { + lastPath = path; + composeImage(path); + } + }, CompletableFuture.delayedExecutor(DEBOUNCE_TIME, TimeUnit.SECONDS, mc)); + }) + .build() + ); + + public final Setting 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) { + // Parse URI + String parsed = path.replace("\"", "").replace("\\", "/"); + String name = parsed.substring(parsed.lastIndexOf("/") + 1); + cachedImageData = null; + + currentImageDataFuture = CompletableFuture.supplyAsync(() -> { + try { + InputStream imageFile = path.toLowerCase().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) { + throw new RuntimeException(e); + } + }, worker).exceptionallyAsync( e -> { + LOG.debug("Failed to load image", e); + texture = null; + return null; + }, mc).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); + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/FrameMetadata.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/FrameMetadata.java new file mode 100644 index 0000000000..bffe4d51ee --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/FrameMetadata.java @@ -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) { +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/FrameMetadataFactory.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/FrameMetadataFactory.java new file mode 100644 index 0000000000..42f642b03d --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/FrameMetadataFactory.java @@ -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; + } + +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/ImageData.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/ImageData.java new file mode 100644 index 0000000000..79b0beec20 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/ImageData.java @@ -0,0 +1,31 @@ +/* + * 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 it.unimi.dsi.fastutil.ints.IntList; +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 IntList delays; + + public ImageData(String name) { + this.name = name; + } + + public int getColumns(){ + return (int) Math.ceil(totalFrames / (double) framesPerColumn); + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/ImageDataFactory.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/ImageDataFactory.java new file mode 100644 index 0000000000..57472428f4 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/ImageDataFactory.java @@ -0,0 +1,113 @@ +/* + * 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 it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; + +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; + + +import static meteordevelopment.meteorclient.MeteorClient.LOG; +import static meteordevelopment.meteorclient.systems.hud.elements.ImageHud.MAX_TEX_SIZE; +import static meteordevelopment.meteorclient.utils.misc.texture.Offset.getFrameOffset; + +public class ImageDataFactory { + /** + * Creates an ImageData object from a GIF. If support for other animated formats is to be added, another similar method + * should be used (fromWEBP, fromAPNG, ...) + */ + public static ImageData fromGIF(String name, ImageReader reader) throws IOException { + if (reader == null || !reader.getFormatName().equalsIgnoreCase("gif")) { + throw new IOException("Invalid image format"); + } + ImageData imageData = new ImageData(name); + imageData.width = reader.getWidth(0); + imageData.height = reader.getHeight(0); + imageData.framesPerColumn = MAX_TEX_SIZE / imageData.height; + imageData.totalFrames = reader.getNumImages(true); + imageData.canvasWidth = imageData.width * imageData.getColumns(); + imageData.canvasHeight = Math.min((imageData.height * imageData.totalFrames), MAX_TEX_SIZE); + LOG.debug("Canvas height {} {} {} {}", imageData.canvasHeight, imageData.height, imageData.framesPerColumn, MAX_TEX_SIZE); + imageData.delays = new IntArrayList(); + BufferedImage image = composeGIF(reader,imageData); + imageData.texture = TextureUtils.bufferedToNative(image); + return imageData; + } + + /** + * Creates an ImageData object from a regular static image. Probably overkill, but helps with consistency. + */ + public static ImageData fromStatic(String name, ImageReader reader) throws IOException { + ImageData imageData = new ImageData(name); + imageData.width = reader.getWidth(0); + imageData.height = reader.getHeight(0); + imageData.framesPerColumn = 1; + imageData.totalFrames = 1; + imageData.canvasWidth = imageData.width; + imageData.canvasHeight = imageData.height; + imageData.delays = new IntArrayList(); + imageData.texture = TextureUtils.bufferedToNative(reader.read(0)); + return imageData; + } + + /** + * Reads each frame of the GIF and composes it depending on the disposal method used in that frame. + * @return a BufferedImage consisting of all frames stacked by rows and then by columns. + */ + private static BufferedImage composeGIF(ImageReader reader, ImageData imageData) throws IOException { + BufferedImage canvas = new BufferedImage(imageData.canvasWidth, imageData.canvasHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D canvasGraphics = canvas.createGraphics(); + int lastUndisposed = 0; + for (int i = 0; i < imageData.totalFrames; i++) { + IIOMetadata metadata = reader.getImageMetadata(i); + FrameMetadata frameMetadata = FrameMetadataFactory.fromGIF(metadata); + imageData.delays.add(frameMetadata.delay()); + Offset offset = getFrameOffset(i, imageData.framesPerColumn, imageData.width, imageData.height); + switch(frameMetadata.disposal()) { + case "doNotDispose" -> { // Set the background to the current undisposed frame and draw the next frame over. + Offset prevOffset = (i==0) ? new Offset(0,0) : getFrameOffset(lastUndisposed, imageData.framesPerColumn, imageData.width, imageData.height); + drawWithDisposal(reader.read(i),canvas,canvasGraphics,frameMetadata,offset,prevOffset,imageData.width,imageData.height); + lastUndisposed = i; + } + case "restoreToPrevious" -> { // Set the background to the previous undisposed frame and draw the next frame over. + Offset prevOffset = getFrameOffset(lastUndisposed, imageData.framesPerColumn,imageData.width, imageData.height); + drawWithDisposal(reader.read(i),canvas,canvasGraphics,frameMetadata,offset,prevOffset,imageData.width,imageData.height); + } + case "restoreToBackground" -> { // Set the background color and draw the next frame over. + canvasGraphics.setColor(frameMetadata.backgroundColor()); + canvasGraphics.fillRect(offset.x(), offset.y(), imageData.width, imageData.height); + canvasGraphics.drawImage(reader.read(i),offset.x()+frameMetadata.offset().x(), offset.y()+frameMetadata.offset().y(), null); + } + default -> canvasGraphics.drawImage(reader.read(i),offset.x(), offset.y(), null); // Just draw the next frame as fallback. + } + } + canvasGraphics.dispose(); + return canvas; + } + + /** + * Taking into account the disposal type and the offsets of the frame, takes one base frame and draws another over it. + * Instead of making a BufferedImage for each frame, the base canvas is used and the frames are cut from there. + */ + private static void drawWithDisposal(BufferedImage next, BufferedImage canvas, Graphics2D canvasGraphics, + FrameMetadata frameMetadata, Offset offset, Offset prevOffset, int width, int height) { + canvasGraphics.drawImage(canvas, + offset.x(), offset.y(), + offset.x()+width, offset.y()+height, + prevOffset.x(),prevOffset.y(), + prevOffset.x()+width,prevOffset.y()+height + ,null); + canvasGraphics.drawImage(next, + offset.x() + frameMetadata.offset().x(), offset.y() + frameMetadata.offset().y(), + null); + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/Offset.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/Offset.java new file mode 100644 index 0000000000..3b74b093aa --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/Offset.java @@ -0,0 +1,21 @@ +/* + * 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; + +public record Offset(int x, int y) { + /** + * Get the offset in the animated atlas for a certain frame in an animated texture. The atlas adds frames row first + * before adding new columns. + * @return Offset. + */ + public static Offset getFrameOffset(int i, int framesPerColumn, int width, int height) { + int col = i / framesPerColumn; + int row = i % framesPerColumn; + int x = col * width; + int y = row * height; + return new Offset(x,y); + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/TextureUtils.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/TextureUtils.java new file mode 100644 index 0000000000..a472d8cb7a --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/texture/TextureUtils.java @@ -0,0 +1,57 @@ +/* + * 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 net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; + +import java.awt.image.BufferedImage; +import java.util.List; + +import static meteordevelopment.meteorclient.MeteorClient.*; + +public class TextureUtils { + /** + * Registers the texture from an imageData object parsing for invalid chars. + * @return Identifier of the registered texture. + */ + public static Identifier registerTexture(ImageData imageData) { + String name = imageData.name.toLowerCase().replaceAll("[^a-z0-9/._-]","_"); + Identifier identifier = Identifier.of(MOD_ID,name); + NativeImageBackedTexture texture = new NativeImageBackedTexture(() -> imageData.name, imageData.texture); + mc.getTextureManager().registerTexture(identifier, texture); + return identifier; + } + + /** + * Registers the texture from an imageData object parsing for invalid chars. + * @return Identifier of the registered texture. + */ + public static NativeImage bufferedToNative(BufferedImage canvas) { + NativeImage tex = new NativeImage(canvas.getWidth(), canvas.getHeight(), true); + for (int x = 0; x < canvas.getWidth(); x++) { + for (int y = 0; y < canvas.getHeight(); y++) { + tex.setColorArgb(x, y, canvas.getRGB(x, y)); + } + } + return tex; + } + + /** + * Calculates the frame of an animated texture taking into account the delays in centiseconds and the system time. + */ + public static int getCurrentAnimationFrame(List delays) { + int total = 0; + long time = Util.getMeasuringTimeMs() % delays.stream().mapToInt(d -> d * 10).sum(); + for (int i = 0; i < delays.size(); i++) { + total += delays.get(i) * 10; + if (time < total) return i; + } + return 0; + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/render/RenderUtils.java b/src/main/java/meteordevelopment/meteorclient/utils/render/RenderUtils.java index f0a7a38a4d..5fbe1390f2 100644 --- a/src/main/java/meteordevelopment/meteorclient/utils/render/RenderUtils.java +++ b/src/main/java/meteordevelopment/meteorclient/utils/render/RenderUtils.java @@ -8,13 +8,19 @@ import meteordevelopment.meteorclient.MeteorClient; import meteordevelopment.meteorclient.events.render.Render3DEvent; import meteordevelopment.meteorclient.events.world.TickEvent; +import meteordevelopment.meteorclient.gui.renderer.packer.TextureRegion; +import meteordevelopment.meteorclient.renderer.Renderer2D; import meteordevelopment.meteorclient.renderer.ShapeMode; +import meteordevelopment.meteorclient.systems.hud.HudRenderer; import meteordevelopment.meteorclient.utils.PostInit; import meteordevelopment.meteorclient.utils.misc.Pool; import meteordevelopment.meteorclient.utils.render.color.Color; import meteordevelopment.orbit.EventHandler; import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.util.math.MatrixStack; import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; import org.joml.Matrix3x2fStack; diff --git a/src/main/resources/assets/meteor-client/textures/icons/gui/default_image.png b/src/main/resources/assets/meteor-client/textures/icons/gui/default_image.png new file mode 100644 index 0000000000..4ab006c511 Binary files /dev/null and b/src/main/resources/assets/meteor-client/textures/icons/gui/default_image.png differ diff --git a/src/main/resources/assets/meteor-client/textures/icons/gui/loading_image.png b/src/main/resources/assets/meteor-client/textures/icons/gui/loading_image.png new file mode 100644 index 0000000000..36ea8d2a48 Binary files /dev/null and b/src/main/resources/assets/meteor-client/textures/icons/gui/loading_image.png differ