Overlay HUD

Permanently display information on the player's HUD — health bars, faction stats, visual effects — without interrupting gameplay.

fr.eri.eriapi.overlay Client-side 1920x1080 design

What is an overlay?

An overlay is a graphical element displayed permanently on the player's screen, on top of the game and Minecraft's vanilla HUD. Unlike a GUI (EriGuiScreen) which takes control of the screen, an overlay stays visible at all times while the player is playing.

Typical use cases:

  • Resource bars — mana, stamina, energy, heat
  • Faction HUD — faction name, rank, territory points
  • Status indicators — active effects, buffs/debuffs
  • Custom minimap or compass
  • Decorative effects — particles, aurora, ambient fog

Overlay vs GUI — what is the difference?

Characteristic EriGuiScreen (GUI) EriOverlay (HUD)
Opening On player action (key, command) Permanent (registered once)
Blocks gameplay Yes — the player cannot move No — the player plays normally
Interactions Keyboard, mouse, clicks None (render only)
Coordinates 1920x1080 design pixels 1920x1080 design pixels (identical)
Components All EriAPI components Visual components only (no input)
Lifecycle Open / close Register / unregister + show / hide
Same component system

Overlays use exactly the same components, the same 1920x1080 coordinate system and the same animations as a regular GUI. If you know how to create an EriGuiScreen, you already know how to create an overlay.

EriOverlay

EriOverlay is the abstract base class for all HUD overlays. You create a subclass, define the components in buildOverlay(), and register it with OverlayManager.

CLASS ABSTRACT
EriOverlay
fr.eri.eriapi.overlay.EriOverlay

Creating an overlay

Java — Minimal overlay
import fr.eri.eriapi.overlay.*;
import fr.eri.eriapi.gui.components.*;

public class HealthOverlay extends EriOverlay {

    public HealthOverlay() {
        // unique id, width and height in design pixels
        super("health_hud", 200, 60);

        setAnchor(Anchor.BOTTOM_LEFT);   // bottom-left corner
        setOffset(10, -10);              // offset from the anchor
    }

    @Override
    protected void buildOverlay() {
        getRoot().add(
            new Rectangle(0, 0, 200, 60)
                .fillColor(0x80000000)
                .cornerRadius(8),
            new Label(10, 20, 180, 20, "HP : 20")
                .color(0xFFFF5555)
        );
    }
}

// Registration (once, in ClientProxy.init() for example):
OverlayManager.getInstance().register(new HealthOverlay());

Lifecycle

The buildOverlay() method is called only once on the first render (lazy initialization). Subsequent calls to render() reuse the already-built components. If you want to force a rebuild (data that changes), call rebuild().

EriOverlay — methods
fr.eri.eriapi.overlay.EriOverlay
Public API
  • EriOverlay(String id, int designWidth, int designHeight)
    Constructor. id is the unique identifier used by OverlayManager. designWidth and designHeight define the overlay area in design pixels (1920x1080).
    Constructor
  • void buildOverlay()
    Abstract method to implement. Call getRoot().add(...) to add components. Called only once on first render.
    To implement
  • ContainerComponent getRoot()
    Returns the root container. Use from buildOverlay() to add components.
    Getter
  • void rebuild()
    Forces the overlay to be rebuilt on the next render. Useful to refresh the display after a significant data change.
    Method
  • EriOverlay setAnchor(Anchor anchor)
    Sets the anchor point of the overlay on screen. See the Anchor section. Default: TOP_LEFT.
    Fluent
  • EriOverlay setOffset(int offsetX, int offsetY)
    Offset in design pixels from the anchor. Positive = down/right, negative = up/left.
    Fluent
  • EriOverlay setLayer(OverlayLayer layer)
    Sets the render layer (moment in the HUD pipeline). Default: POST_ALL.
    Fluent
  • EriOverlay hideInGui(boolean hide)
    If true (default), the overlay disappears when a GUI is open. Set to false to always display the overlay even in menus.
    Fluent
  • EriOverlay hideOnF1(boolean hide)
    If true (default), the overlay disappears when the player presses F1 (hide HUD). Set to false to ignore F1.
    Fluent
  • void show() / hide() / toggle()
    Direct visibility control for this overlay. Equivalent to OverlayManager.getInstance().show("id").
    Method

OverlayManager

OverlayManager is the singleton that centralizes all registered overlays. It automatically handles rendering and ticks via Forge events — you have nothing to configure beyond the initial registration.

SINGLETON
OverlayManager
fr.eri.eriapi.overlay.OverlayManager
Java — Registration and control
import fr.eri.eriapi.overlay.OverlayManager;

OverlayManager om = OverlayManager.getInstance();

// Register overlays (once, during init)
om.register(new HealthOverlay());
om.register(new ManaOverlay());
om.register(new FactionHudOverlay());

// Visibility control by ID
om.show("health_hud");
om.hide("health_hud");
om.toggle("health_hud");

// Global control
om.hideAll();
om.showAll();

// Retrieve an overlay to modify it
EriOverlay overlay = om.get("health_hud");
if (overlay != null) {
    overlay.rebuild(); // Force a rebuild
}

// Unregister an overlay
om.unregister("health_hud");
OverlayManager — methods
fr.eri.eriapi.overlay.OverlayManager
Singleton
  • static OverlayManager getInstance()
    Returns the unique manager instance.
    Static
  • void register(EriOverlay overlay)
    Registers an overlay. If an overlay with the same ID already exists, it is replaced. Registration order determines render order.
    Method
  • void unregister(String id)
    Removes a registered overlay. No effect if the ID is unknown.
    Method
  • EriOverlay get(String id)
    Returns the overlay matching the ID, or null if it does not exist.
    Getter
  • void show(String id) / hide(String id) / toggle(String id)
    Controls the visibility of an overlay by its ID. No effect if the ID is unknown.
    Method
  • void showAll() / hideAll()
    Shows or hides all registered overlays.
    Method
Where to register overlays?

Register your overlays in ClientProxy.init() (or postInit() if you need items/blocks to be loaded first). Do not register them in a GUI constructor or in an event that fires frequently — registration is done only once.

Anchor

The Anchor enum defines the nine possible anchor points for an overlay. The anchor is the corner or edge of the screen from which the overlay is positioned. Combine with setOffset(x, y) to fine-tune the positioning.

ENUM
Anchor
fr.eri.eriapi.overlay.Anchor
Value Position on screen Typical use case
TOP_LEFT Top-left corner (0, 0) Minimap, mod logo
TOP_CENTER Center of the top edge Zone title, boss bar
TOP_RIGHT Top-right corner Clock, coordinates, ping
CENTER_LEFT Center of the left edge Side status bar
CENTER Absolute center of the screen Custom crosshair, central alert
CENTER_RIGHT Center of the right edge Buff/debuff list
BOTTOM_LEFT Bottom-left corner Mana/stamina bar
BOTTOM_CENTER Center of the bottom edge Action bar, shortcuts
BOTTOM_RIGHT Bottom-right corner Faction stats, faction name
Java — Using anchor with offset
// Overlay in bottom-right with 15px margin
new MonOverlay()
    .setAnchor(Anchor.BOTTOM_RIGHT)
    .setOffset(-15, -15);  // negative = toward top/left

// Overlay centered at top with 20px margin from edge
new BossBarOverlay()
    .setAnchor(Anchor.TOP_CENTER)
    .setOffset(0, 20);     // positive = toward bottom
Design coordinates

Offsets are in design pixels (1920x1080 space), just like GUI components. EriAPI automatically converts them to the player's actual resolution. An offset of (-15, -15) will represent the same visual margin on a 1080p or 4K screen.

OverlayLayer

OverlayLayer defines at which point in Minecraft's HUD render pipeline your overlay is drawn. This determines what is displayed above or below it.

ENUM
OverlayLayer
fr.eri.eriapi.overlay.OverlayLayer
Value Render moment Result
PRE_ALL Before all vanilla HUD elements Your overlay is drawn beneath the health bar, hotbar, etc.
POST_HOTBAR After the item bar (hotbar) Drawn above the hotbar but below the health bar
POST_EXPERIENCE After the experience bar Drawn after the XP bar, before other elements
POST_ALL After all vanilla HUD elements (default) Your overlay is drawn on top of everything in the vanilla HUD
Java — Changing the render layer
// Display the overlay on top of the entire HUD (default behavior)
overlay.setLayer(OverlayLayer.POST_ALL);

// Display just after the hotbar
overlay.setLayer(OverlayLayer.POST_HOTBAR);

// Ambient background beneath the entire HUD
overlay.setLayer(OverlayLayer.PRE_ALL);

Components available in overlays

Overlays reuse exactly the same components as GUIs. The only difference: interactive components (TextField, ScrollList, TabView, etc.) serve no purpose in an overlay since there is no keyboard/mouse interaction with the HUD.

Shapes and backgrounds

Rectangle
fr.eri.eriapi.gui.components.Rectangle
Shape

Solid rectangle with optional rounded corners and border. Perfect for HUD panel backgrounds.

Java
new Rectangle(0, 0, 200, 60)
    .fillColor(0x80000000)   // semi-transparent background
    .borderColor(0x40FFFFFF) // subtle border
    .cornerRadius(8);
GradientRectangle
fr.eri.eriapi.gui.components.GradientRectangle
Shape

Rectangle with a linear gradient (vertical or horizontal). Ideal for ambient bars or gradient backgrounds.

Java
// Vertical gradient purple -> blue
new GradientRectangle()
    .originalPos(0, 0).originalSize(200, 60)
    .vertical(0xFF6B2FA0, 0xFF00E5FF)
    .cornerRadius(8);

// Horizontal gradient
new GradientRectangle()
    .originalPos(0, 0).originalSize(300, 8)
    .horizontal(0xFF00E5FF, 0x00000000); // fade to transparent
Circle / Triangle
fr.eri.eriapi.gui.components
Shape

Filled circle or triangle (directional indicator). Useful for icons or navigation arrows.

Text

Label
fr.eri.eriapi.gui.components.Label
Text

Text display with scale, alignment and shadow. The basic text component for all HUDs.

Java
new Label(10, 10, 180, 20, "Faction: Erinium")
    .color(0xFFFFD700)
    .scale(1.2f)
    .shadow(true);

Progress controls

ProgressBar
fr.eri.eriapi.gui.components.ProgressBar
Control

Progress bar with animated fill, colors and rounded corners. The ideal component for displaying resources (mana, health, exp).

Java
ProgressBar manaBar = new ProgressBar(0, 0, 180, 14)
    .progress(0.75f)                // 75% mana
    .fillColor(0xFF6B2FA0)          // fill color
    .backgroundColor(0x40000000)   // background
    .cornerRadius(7)
    .showText(false);               // no text in overlay

Containers

ContainerComponent
fr.eri.eriapi.gui.core.ContainerComponent
Layout

Container for nesting components. Allows grouping elements and applying a collective opacity or animations. The getRoot() method of EriOverlay already returns a root ContainerComponent — use sub-containers to organize your HUD into logical sections.

Decorative visual effects

These components are particularly suited to overlays: they animate continuously, consume few resources and provide visual ambiance without requiring any AnimationManager configuration.

Aurora
fr.eri.eriapi.gui.components.Aurora
Visual effect

Undulating northern-lights bands with a vertical gradient. Up to 10 stackable bands, each with its own color, frequency, amplitude and speed. Animated continuously via System.currentTimeMillis() — no EriAPI animation needed.

Java — Aurora as background overlay
new Aurora()
    .originalPos(0, 0).originalSize(1920, 400)
    .addBand(0x406B2FA0, 0.3f, 1.2f, 80, 50)   // purple
    .addBand(0x3000E5FF, 0.5f, 0.8f, 60, 120)  // cyan
    .addBand(0x20FF6B6B, 0.7f, 0.6f, 50, 200)  // subtle pink
    .speed(0.02f);
// addBand parameters: color, frequency, amplitude, height, offsetY
Starfield
fr.eri.eriapi.gui.components.Starfield
Visual effect

Star field with sinusoidal twinkle and random shooting stars. Up to 3 simultaneous shooting stars.

Java
new Starfield()
    .originalPos(0, 0).originalSize(600, 400)
    .starCount(80)
    .starColor(0xFFF0F2FF)
    .shootingStarChance(0.003f); // 0 = never, 0.01 = frequent
ParticleSystem
fr.eri.eriapi.gui.components.ParticleSystem
Visual effect

Soft particle system (circles with radial gradient) that spawn, drift and fade out. Additive blending for a glowing effect. Perfect for a magical ambiance.

Java
new ParticleSystem()
    .originalPos(0, 0).originalSize(300, 100)
    .maxParticles(20)
    .spawnRate(0.3f)
    .particleLife(2000, 4000)
    .particleSize(20, 60)
    .particleColor(0x306B2FA0)
    .drift(0, -0.008f)     // rises gently
    .spread(0.01f)
    .fadeIn(0.2f).fadeOut(0.4f);
SmokeFog
fr.eri.eriapi.gui.components.SmokeFog
Visual effect

Organic fog based on fractal Simplex noise (FBM). Animated continuously, the fog flows and deforms naturally. Very subtle at low intensity.

Java
new SmokeFog()
    .originalPos(0, 0).originalSize(400, 200)
    .color(0x6B2FA0)      // RGB color
    .intensity(0.08f)     // max alpha (very subtle)
    .scale(0.006f)        // wisp size
    .speed(0.0003f)
    .octaves(3)
    .cellSize(12);

Animations and the "overlay" scope

All EriAPI animations work in overlays. However, there is an important subtlety: when a GUI is closed, EriGuiScreen.onGuiClosed() calls AnimationManager.getInstance().stopGuiAnimations() — this stops all animations that have no scope.

To protect overlay animations from being stopped, use .scope("overlay"):

Java — Protected overlay animation with .scope()
public class ManaOverlay extends EriOverlay {

    private Label manaLabel;
    private ProgressBar manaBar;

    public ManaOverlay() {
        super("mana_hud", 200, 40);
        setAnchor(Anchor.BOTTOM_LEFT);
        setOffset(10, -70); // above the vanilla health bar
    }

    @Override
    protected void buildOverlay() {
        manaLabel = new Label(10, 5, 100, 15, "Mana")
            .color(0xFF9B59B6);

        manaBar = new ProgressBar(10, 22, 180, 10)
            .progress(1f)
            .fillColor(0xFF6B2FA0)
            .backgroundColor(0x40000000)
            .cornerRadius(5);

        getRoot().add(manaLabel, manaBar);

        // Breathing animation — scope "overlay" so it is not stopped
        // when the player opens/closes a GUI
        Animation anim = Animation.create(0.6f, 1f, 30)
            .pingPong()
            .scope("overlay")
            .easing(Easing.EASE_IN_OUT)
            .onUpdate(v -> manaLabel.setOpacity(v));
        AnimationManager.getInstance().play(anim);
    }

    // Public method to update the mana value
    public void setMana(float ratio) {
        if (manaBar != null) {
            manaBar.progress(ratio);
        }
    }
}
Always use .scope("overlay") for looping animations

Without this scope, your pingPong() or loop() animations will be interrupted every time the player closes a GUI (inventory, etc.). With .scope("overlay"), they keep running independently.

Component animation helpers

The pre-built methods on Component (fadeIn, breathe, pulse, etc.) also work in overlays, but they do not automatically define a scope. For looping animations in an overlay, create the animation manually with .scope("overlay").

Java — Comparison: with and without scope
// ❌ Will be stopped when the player closes a GUI
label.breathe(40); // internal helper, no scope

// ✅ Continues even when a GUI is opened then closed
Animation.create(0.4f, 1f, 20)
    .pingPong()
    .scope("overlay")
    .easing(Easing.EASE_IN_OUT)
    .onUpdate(v -> label.setOpacity(v));
AnimationManager.getInstance().play(breatheAnim);

Example 1 — Mana bar

A simple overlay with a progress bar and a label, anchored to the bottom-left. The bar can be updated from anywhere in the code.

Java — Complete ManaOverlay
import fr.eri.eriapi.overlay.*;
import fr.eri.eriapi.gui.components.*;

public class ManaOverlay extends EriOverlay {

    private ProgressBar manaBar;
    private Label manaLabel;

    public ManaOverlay() {
        super("mana_overlay", 210, 50);
        setAnchor(Anchor.BOTTOM_LEFT);
        setOffset(10, -80); // just above the health bar
        hideInGui(true);    // hide when GUI is open
        hideOnF1(true);     // hide when F1 is pressed
    }

    @Override
    protected void buildOverlay() {
        // Background
        getRoot().add(new Rectangle(0, 0, 210, 50)
            .fillColor(0x90000000)
            .cornerRadius(6));

        // "Mana" label
        manaLabel = new Label(8, 6, 60, 12, "MANA")
            .color(0xFFBB86FC)
            .scale(0.8f)
            .shadow(true);

        // Mana bar
        manaBar = new ProgressBar(8, 24, 194, 12)
            .progress(1f)
            .fillColor(0xFF6B2FA0)
            .backgroundColor(0x50000000)
            .cornerRadius(6);

        // Value label
        Label valueLabel = new Label(8, 38, 194, 10, "100 / 100")
            .color(0x80FFFFFF)
            .scale(0.75f)
            .align(Label.Align.CENTER);

        getRoot().add(manaLabel, manaBar, valueLabel);
    }

    /** Called from your mana system to update the bar. */
    public void setMana(int current, int max) {
        if (manaBar != null) {
            manaBar.progress((float) current / max);
        }
    }
}

// In ClientProxy.init():
ManaOverlay manaOverlay = new ManaOverlay();
OverlayManager.getInstance().register(manaOverlay);

// From anywhere in your mod (client side):
ManaOverlay overlay = (ManaOverlay) OverlayManager.getInstance().get("mana_overlay");
if (overlay != null) overlay.setMana(75, 100);

Example 2 — Faction HUD

A faction HUD in the bottom-right corner showing the faction name, the player's rank and the number of members online.

Java — FactionHudOverlay
import fr.eri.eriapi.overlay.*;
import fr.eri.eriapi.gui.components.*;

public class FactionHudOverlay extends EriOverlay {

    private Label factionNameLabel;
    private Label rankLabel;
    private Label membersLabel;

    public FactionHudOverlay() {
        super("faction_hud", 220, 70);
        setAnchor(Anchor.BOTTOM_RIGHT);
        setOffset(-10, -10);
    }

    @Override
    protected void buildOverlay() {
        // Gradient background
        getRoot().add(new GradientRectangle()
            .originalPos(0, 0).originalSize(220, 70)
            .vertical(0x80150A2A, 0x801A0B35)
            .cornerRadius(8));

        // Top border
        getRoot().add(new GradientRectangle()
            .originalPos(0, 0).originalSize(220, 2)
            .horizontal(0x00000000, 0xFF6B2FA0)
            .cornerRadius(0));

        // Icon / prefix
        getRoot().add(new Label(8, 10, 20, 16, "\u2726")
            .color(0xFFFFD700)
            .scale(1.0f));

        // Faction name
        factionNameLabel = new Label(28, 8, 180, 18, "No faction")
            .color(0xFFE0E0E0)
            .scale(1.0f)
            .shadow(true);

        // Rank
        rankLabel = new Label(8, 30, 204, 14, "Rank: ---")
            .color(0xFF9B8BCC)
            .scale(0.85f);

        // Members online
        membersLabel = new Label(8, 48, 204, 14, "Members online: 0")
            .color(0xFF6BCB77)
            .scale(0.85f);

        getRoot().add(factionNameLabel, rankLabel, membersLabel);
    }

    public void update(String faction, String rank, int onlineMembers) {
        if (factionNameLabel != null) factionNameLabel.text(faction);
        if (rankLabel != null) rankLabel.text("Rank: " + rank);
        if (membersLabel != null) membersLabel.text("Members online: " + onlineMembers);
    }
}

Example 3 — Overlay with animated visual effects

An ambient overlay with a star field background and an aurora, displayed at the top of the screen. This type of overlay is used for permanent decorative effects.

Java — AmbientOverlay with Starfield and Aurora
import fr.eri.eriapi.overlay.*;
import fr.eri.eriapi.gui.components.*;
import fr.eri.eriapi.gui.anim.*;

public class AmbientOverlay extends EriOverlay {

    public AmbientOverlay() {
        super("ambient_overlay", 1920, 300);
        setAnchor(Anchor.TOP_LEFT);
        setLayer(OverlayLayer.PRE_ALL); // beneath the vanilla HUD
        hideInGui(false); // stays visible even in menus
        hideOnF1(true);
    }

    @Override
    protected void buildOverlay() {
        // Star field background
        getRoot().add(new Starfield()
            .originalPos(0, 0).originalSize(1920, 300)
            .starCount(60)
            .starColor(0xFFF0F2FF)
            .shootingStarChance(0.002f));

        // Aurora on top
        getRoot().add(new Aurora()
            .originalPos(0, 0).originalSize(1920, 300)
            .addBand(0x306B2FA0, 0.4f, 1.0f, 100, 80)
            .addBand(0x2000E5FF, 0.6f, 0.7f, 80, 160)
            .speed(0.015f));
    }
}

Technical notes

Animation scope

When a GUI is closed (EriGuiScreen.onGuiClosed()), the framework calls AnimationManager.getInstance().stopGuiAnimations(). This method stops all animations without a scope (scope == null). Animations with .scope("overlay") (or any other non-null scope) are preserved.

Rule to remember: in an overlay, every looping animation (loop(), pingPong()) must have .scope("overlay"). One-shot animations (fade-in, slide) do not need a scope.

hideInGui

By default, hideInGui(true) automatically hides the overlay when Minecraft.getMinecraft().currentScreen != null (a GUI is open). Set to false if your overlay must remain visible in menus (decorative effect, persistent information).

hideOnF1

By default, hideOnF1(true) hides the overlay when mc.gameSettings.hideGUI == true (player presses F1 to hide the HUD). Set to false only for overlays that have a strong reason to always display (debug info, etc.).

Thread safety

Overlays are entirely rendered on the client render thread. Do not modify components from an async thread (EriScheduler.async or similar). Use EriScheduler.delay(0, () -> overlay.setMana(...)) to dispatch back to the main thread if necessary.

Performance

Overlays are rendered every frame (60 FPS+). Avoid heavy computations in buildOverlay() or in onUpdate lambdas. Decorative components (SmokeFog, ParticleSystem) have built-in limits (cellSize, maxParticles) to control GPU cost.

Best practice: update via public methods

Store references to components as private fields of your overlay class. Expose public methods (setMana(), update()) to update values. Never create new components on every tick — only modify the properties of existing components (.text(), .progress(), etc.).

OverlayMod — Modular System

OverlayMod is EriAPI's new modular overlay system. It replaces the manual approach with EriOverlay for complex overlays that need user configuration, drag & drop positioning and automatic persistence between game sessions.

Each OverlayMod is a self-contained module that includes:

  • Its own position — stored in design pixels (1920x1080 space), converted at render time by the ScaleManager
  • Its own zoom factor — each mod has its own configurable scale
  • Configurable settings — via ModSettings, exposed in the editor
  • Drag & drop positioning — the built-in editor lets the user freely move each mod
  • Automatic JSON persistence — position, scale and settings are saved and restored
  • Per-frame rendering with nanotime precision — the onFrame() callback receives the delta in nanoseconds
CLASS ABSTRACT
OverlayMod
fr.eri.eriapi.overlay.OverlayMod

Creating an OverlayMod

Java — Minimal OverlayMod (health display)
import fr.eri.eriapi.overlay.OverlayMod;
import fr.eri.eriapi.gui.components.*;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayer;

public class HealthOverlayMod extends OverlayMod {

    private Label healthLabel;
    private Rectangle background;

    public HealthOverlayMod() {
        super("my_health", "Health Display", 200, 40);
        setCategory("Combat");
        setDescription("Displays the player's health points.");
        setDefaultPosition(0.01f, 0.9f);  // bottom-left (x and y as screen fraction)
        setMinScale(0.5f);
        setMaxScale(2.0f);
    }

    @Override
    protected void buildOverlay() {
        background = new Rectangle();
        background.originalPos(0, 0).originalSize(200, 40);
        background.fillColor(0xAA000000).cornerRadius(6);
        getRoot().add(background);

        healthLabel = new Label("HP: 20/20");
        healthLabel.originalPos(8, 8).originalSize(184, 24);
        healthLabel.color(0xFFFF5555).scale(1.2f);
        getRoot().add(healthLabel);
    }

    @Override
    protected void onFrame(float partialTicks, long deltaNanos) {
        EntityPlayer player = Minecraft.getMinecraft().player;
        if (player == null) return;
        int hp = (int) player.getHealth();
        int maxHp = (int) player.getMaxHealth();
        healthLabel.text("HP: " + hp + "/" + maxHp);
    }
}

Registration

Register your mods in ClientProxy.init() via OverlayModManager. As with OverlayManager, registration is done only once.

Java — Registration in ClientProxy.init()
import fr.eri.eriapi.overlay.OverlayModManager;

// In ClientProxy.init():
OverlayModManager.getInstance().register(new HealthOverlayMod());
OverlayModManager.getInstance().register(new DebugOverlayMod());
OverlayModManager.getInstance().register(new FactionInfoMod());
OverlayMod — method reference
fr.eri.eriapi.overlay.OverlayMod
Public API
  • OverlayMod(String id, String name, int designWidth, int designHeight)
    Constructor. id is the unique identifier (used for JSON persistence). name is the display name shown in the editor. designWidth and designHeight are the render area dimensions in design pixels (1920x1080 space).
    Constructor
  • void setCategory(String category)
    Sets the category shown in the manager (e.g. "Combat", "Information", "Debug"). Allows grouping mods by theme in the management interface.
    Configuration
  • void setDescription(String description)
    Short description shown in the overlay manager. Should explain in one sentence what the mod displays.
    Configuration
  • void setDefaultPosition(float x, float y)
    Default position as a screen fraction (0.0 to 1.0 for x and y). Compatible with the legacy format — internally converts to design pixels (x * 1920, y * 1080). Example: (0.01f, 0.9f) = approximately bottom-left corner.
    Configuration
  • void setDefaultPositionDesign(float designX, float designY)
    Default position directly in design pixels (1920x1080 space). Preferred over setDefaultPosition() for precise positioning. Example: (20, 950) = 20 design pixels from the left edge, 950 from the top.
    Configuration
  • void setMinScale(float min) / setMaxScale(float max)
    Sets the user zoom limits in the editor. Typical values: min 0.5f, max 2.0f. Default scale is 1.0f.
    Configuration
  • void buildOverlay()
    Abstract method to implement. Build all visual components here with getRoot().add(...). Called once on first render (and on every rebuild() call).
    To implement
  • void buildSettings(ModSettings settings)
    Optional override. Declare the mod's configurable settings here via the ModSettings builder. See the ModSettings section.
    Optional
  • void onFrame(float partialTicks, long deltaNanos)
    Optional override. Called every render frame (60-144+ times per second). partialTicks is the fraction of a tick elapsed since the last tick (0.0 to 1.0). deltaNanos is the time in nanoseconds since the last call — useful for internal timers. This is where you update labels, bars, etc.
    Optional
  • void onTick()
    Optional override. Called 20 times per second (every server tick). Use for heavy data updates or server synchronizations. See onFrame vs onTick.
    Optional
  • void onSettingChanged(String key, Object value)
    Optional override. Called automatically when the user modifies a setting in the editor. key corresponds to the key declared in buildSettings(). value is the new value (Integer, Float, Boolean or String depending on the setting type).
    Optional
  • <T> T getSetting(String key, T defaultValue)
    Reads the current value of a setting. If the setting has not yet been loaded (first session), returns defaultValue. The type T is inferred from defaultValue — pass the same type as declared in buildSettings().
    Getter
  • void rebuild()
    Forces a complete rebuild of the visual components on the next render. Calls buildOverlay() again. Useful when a setting changes the layout (number of rows, compact mode, etc.).
    Method
  • ContainerComponent getRoot()
    Returns the root container to which components are added. Available from buildOverlay().
    Getter
OverlayMod vs EriOverlay

Use EriOverlay for simple, static overlays (mana bar, faction HUD) where you manage updates yourself via public methods. Use OverlayMod when you want the user to be able to configure, move and enable/disable the overlay from a built-in editor, with automatic persistence. Both systems coexist and can be used together.

ModSettings — Configurable settings

Each OverlayMod can declare configurable settings by implementing buildSettings(ModSettings settings). These settings are automatically exposed in the GuiOverlayHub editor and their values are persisted as JSON.

The ModSettings builder uses a fluent syntax — each call returns the same builder to chain declarations.

CLASS
ModSettings
fr.eri.eriapi.overlay.settings.ModSettings
Java — Declaring settings with buildSettings()
@Override
protected void buildSettings(ModSettings settings) {
    settings
        .separator("Visible sections")
        .toggle("show_fps", "FPS", true)
        .toggle("show_coords", "Coordinates", true)
        .separator("Appearance")
        .colorPicker("text_color", "Text color", 0xFFCCCCCC)
        .slider("bg_opacity", "Background opacity", 0f, 1f, 0.67f)
        .toggle("compact", "Compact mode", false);
}

@Override
protected void onSettingChanged(String key, Object value) {
    if ("text_color".equals(key)) {
        int color = (Integer) value;
        myLabel.color(color);
    } else if ("compact".equals(key)) {
        rebuild();  // The layout changes completely in compact mode
    }
}

// Reading a value in onFrame() or onTick():
boolean showFps = getSetting("show_fps", true);
float opacity  = getSetting("bg_opacity", 0.67f);

Available setting types

Method Return type Description
toggle(key, label, default) Boolean On/off switch. Displays a toggle switch in the editor.
slider(key, label, min, max, default) Float Floating-point slider between min and max. Ideal for opacities, sizes, speeds.
sliderInt(key, label, min, max, default) Integer Integer slider. For counters, line limits, etc.
colorPicker(key, label, defaultColor) Integer (ARGB) Full color picker (wheel, brightness, alpha). The value is an ARGB integer 0xAARRGGBB.
dropdown(key, label, default, options...) String Dropdown menu with a list of choices. Pass options as String varargs.
textField(key, label, default) String Free text field. Useful for prefixes, suffixes or custom names.
keybind(key, label, defaultKey) Integer (LWJGL) Key selector. The value is an LWJGL key code (Keyboard.KEY_*).
separator(title) Visual separator with title. Does not create a setting, used only to organize the interface.
info(text) Non-interactive informational text. Useful for notes or warnings in the editor.
Reading settings with getSetting()

The getSetting(key, defaultValue) method always returns a valid value: either the saved value, or the default value if the setting has never been modified. Use the same default value in buildSettings() and in getSetting() to guarantee consistency.

GuiOverlayHub — The built-in editor

GuiOverlayHub is the central graphical interface of the modular system. It allows users to position, configure and enable/disable their overlays without leaving the game.

GUI
GuiOverlayHub
fr.eri.eriapi.overlay.GuiOverlayHub

Editor features

Editor view (base layer)
Drag & Drop

The base layer of the editor displays all active overlays overlaid on the game screen. Each overlay can be:

  • Moved — click + drag to reposition freely
  • Zoomed — mouse wheel to change the scale
  • Selected — right-click to open the context menu (settings, reset, toggle)

A Snap to grid toggle enables magnetic alignment to a grid. The Reset All button resets all overlays to their default position.

"Manage" button — list popup
List

Opens a popup listing all registered OverlayMods. For each mod:

  • Quick enable/disable toggle
  • Category and description
  • Search bar by name
  • Filter by category
"Settings" button — configuration popup
Settings

Available for each selected mod (via the button in the editor or the right-click context menu). Displays all settings declared in buildSettings() with their corresponding components (toggle, slider, colorPicker, dropdown...). Changes are applied in real time via onSettingChanged().

Opening the editor

The editor opens by default with the ] key (configurable via EriAPI keybindings). It can also be opened programmatically:

Java — Programmatic opening
// Open the editor from code (e.g. from a keybinding or a command)
OverlayModManager.getInstance().openEditor();

Saving

The Save and close button writes the configuration as JSON (see JSON Persistence) and closes the editor. Until the user saves, changes are applied visually in real time but not yet persisted to disk.

onFrame vs onTick

OverlayMod provides two update callbacks with very different frequencies. Choosing the right one directly impacts the smoothness and performance of your overlay.

General rule

onFrame() for everything visual that needs to be smooth. onTick() for heavy computations, world accesses or server-synchronized data.

Callback Frequency When to use it
onFrame(partialTicks, deltaNanos) 60 to 144+ times/s (every render frame) Updating labels, coordinates, FPS, smooth animations, value interpolation
onTick() 20 times/s (every 50ms) Heavy computations, biome access, world queries, server-synchronized data

The deltaNanos parameter of onFrame() gives the exact time elapsed since the last call in nanoseconds. It allows creating internal timers to execute semi-heavy code at precise intervals without using onTick().

Java — nanotime timer in onFrame() for semi-heavy updates
private long timer = 0L;
private static final long INTERVAL = 500_000_000L; // 500ms in nanoseconds

@Override
protected void onFrame(float partialTicks, long deltaNanos) {
    timer += deltaNanos;

    // Light update every frame (smooth)
    fpsLabel.text("FPS: " + Minecraft.getDebugFPS());

    // Heavy update only every 500ms
    if (timer >= INTERVAL) {
        timer = 0L;
        biomeLabel.text("Biome: " + getBiomeName()); // world access, more costly
    }
}
Why onFrame() rather than onTick() for coordinates?

Player coordinates change between ticks (movement is interpolated with partialTicks). If you update coordinates in onTick(), the text jumps in discrete increments. With onFrame(), the text follows the player's actual movement completely smoothly.

Design-pixel positioning

Since the modular system update, OverlayMod positions are stored in design pixels (1920x1080 space), identically to all EriAPI components. The ScaleManager converts these coordinates to actual screen pixels at render time.

Why this change?

The old system stored positions as screen fractions (0.0 to 1.0). Seemingly resolution-independent, this system caused a subtle problem: the visual gap between two adjacent overlays changed depending on the player's resolution and guiScale, because positions and sizes were not in the same coordinate space.

Criterion Old system (fractions) New system (design pixels)
Position unit Screen fraction (0.0 – 1.0) Design pixels (0 – 1920 / 0 – 1080)
Component unit Design pixels (ScaleManager) Design pixels (ScaleManager) — identical
Gap between overlays Varies with resolution Constant regardless of resolution
Conversion at render fraction * displayWidth / guiScale ScaleManager.scaleXf(posXDesign)

Backward compatibility

The public API remains 100% compatible. setDefaultPosition(float x, float y) still accepts fractions and converts them internally:

Java — both positioning methods
// Legacy method — fractions (0.0 to 1.0)
// Still valid, converted internally: x * 1920, y * 1080
setDefaultPosition(0.01f, 0.9f);   // ≈ (19, 972) in design pixels

// New method — direct design pixels (recommended)
// More precise and consistent with the rest of the EriAPI system
setDefaultPositionDesign(20f, 960f);  // exactly 20px from the left, 960px from the top

Example — two adjacent overlays

Java — overlays aligned with a constant gap
// Mana bar at the bottom left
public class ManaBarMod extends OverlayMod {
    public ManaBarMod() {
        super("mana_bar", "Mana", 240, 20);
        setDefaultPositionDesign(20f, 1010f); // 20px from the left, 1010px from the top
    }
}

// Stamina bar just below, 6 design pixels apart
public class StaminaMod extends OverlayMod {
    public StaminaMod() {
        super("stamina_bar", "Stamina", 240, 20);
        // 1010 (mana) + 20 (mana height) + 6 (gap) = 1036
        setDefaultPositionDesign(20f, 1036f);
    }
}
// The 6 design-pixel gap is identical at every resolution.
Position getters

getPosXDesign() and getPosYDesign() return the position in design pixels. The legacy getters getPosXPercent() and getPosYPercent() are still available for backward compatibility (they convert by dividing by 1920/1080).

JSON Persistence

The modular system automatically saves the configuration of each OverlayMod to a JSON file. You have nothing to implement: persistence is entirely managed by OverlayModManager.

File location

Configuration file path
.minecraft/config/eriapi/overlay_mods.json

Data persisted per mod

Data JSON type Description
enabled boolean Whether the mod is active or disabled in the manager
posX / posY float Position as a screen fraction (0.0 to 1.0), independent of resolution
scale float Zoom factor applied by the user in the editor
settings.* mixed Values of all settings declared in buildSettings() (boolean, float, int, String)

Save and load cycle

  • Loading — automatic on the first render frame, after all mods have been registered in ClientProxy.init()
  • Saving — triggered when the user clicks "Save and close" in GuiOverlayHub
Do not edit the JSON file manually

The overlay_mods.json file is generated and read by EriAPI. Manually modifying its structure can cause loading errors. If the file is corrupted, delete it — EriAPI will recreate default values on the next game launch.

Full example — DebugOverlayMod

Here is a real, complete example: the DebugOverlayMod from the EriniumFaction mod. It displays FPS, coordinates, chunk, biome, direction, light and memory in real time. It is a good model to understand how to combine onFrame(), a nanotime timer for heavy data, and buildSettings().

Mod architecture

  • Dimensions: 380 x 160 px (1920x1080 design)
  • Default position: top-left corner (0.5%, 0.5%)
  • Light data (FPS, coords, chunk, direction) — updated in onFrame() every frame
  • Heavy data (biome, light, memory) — updated every 500ms via a nanotime timer in onFrame()
  • 7 configurable settings: 7 visibility toggles, a colorPicker, a slider and a compact toggle
  • Colored FPS: green ≥60, yellow ≥30, red <30
Java — Complete DebugOverlayMod
package fr.eriniumgroup.eriniumfaction.overlay;

import fr.eri.eriapi.gui.components.Label;
import fr.eri.eriapi.gui.components.Rectangle;
import fr.eri.eriapi.overlay.OverlayMod;
import fr.eri.eriapi.overlay.settings.ModSettings;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.chunk.Chunk;

public class DebugOverlayMod extends OverlayMod {

    // Labels updated every frame
    private Label fpsLabel;
    private Label coordsLabel;
    private Label chunkLabel;
    private Label biomeLabel;
    private Label directionLabel;
    private Label lightLabel;
    private Label memoryLabel;

    // Background
    private Rectangle background;

    // nanotime timer for heavy updates (500ms)
    private long heavyUpdateTimer = 0L;
    private static final long HEAVY_UPDATE_INTERVAL = 500_000_000L;

    // Cache for heavy values
    private String cachedBiome   = "";
    private String cachedLight   = "";
    private String cachedMemory  = "";

    public DebugOverlayMod() {
        super("erinium_debug", "Debug Info", 380, 160);
        setCategory("Information");
        setDescription("Displays FPS, coordinates, biome, direction and memory.");
        setDefaultPosition(0.005f, 0.005f); // top-left corner
        setMinScale(0.5f);
        setMaxScale(2.0f);
    }

    @Override
    protected void buildOverlay() {
        int w = 380;

        // Semi-transparent background with rounded corners
        background = new Rectangle();
        background.originalPos(0, 0).originalSize(w, 160);
        background.fillColor(0xAA1A1A2E).cornerRadius(6);
        getRoot().add(background);

        // Creating labels on each row (20px spacing)
        int x = 8, y = 6, lineH = 20;
        fpsLabel       = createLine(x, y, w - 16); y += lineH;
        coordsLabel    = createLine(x, y, w - 16); y += lineH;
        chunkLabel     = createLine(x, y, w - 16); y += lineH;
        biomeLabel     = createLine(x, y, w - 16); y += lineH;
        directionLabel = createLine(x, y, w - 16); y += lineH;
        lightLabel     = createLine(x, y, w - 16); y += lineH;
        memoryLabel    = createLine(x, y, w - 16);

        getRoot().add(fpsLabel, coordsLabel, chunkLabel, biomeLabel,
                      directionLabel, lightLabel, memoryLabel);
    }

    private Label createLine(int x, int y, int w) {
        Label label = new Label("...");
        label.originalPos(x, y).originalSize(w, 18);
        label.color(0xFFCCCCCC).scale(0.9f);
        return label;
    }

    @Override
    protected void buildSettings(ModSettings settings) {
        settings
            .separator("Visible sections")
            .toggle("show_fps",       "FPS",          true)
            .toggle("show_coords",    "Coordinates",  true)
            .toggle("show_chunk",     "Chunk",        true)
            .toggle("show_biome",     "Biome",        true)
            .toggle("show_direction", "Direction",    true)
            .toggle("show_light",     "Light",        true)
            .toggle("show_memory",    "Memory",       true)
            .separator("Appearance")
            .colorPicker("text_color", "Text color",          0xFFCCCCCC)
            .slider("bg_opacity",      "Background opacity",  0f, 1f, 0.67f)
            .toggle("compact",         "Compact mode",        false);
    }

    @Override
    protected void onSettingChanged(String key, Object value) {
        if ("text_color".equals(key)) {
            applyTextColor((Integer) value);
        } else if ("bg_opacity".equals(key)) {
            float opacity = (Float) value;
            int alpha = (int) (opacity * 255);
            background.fillColor((alpha << 24) | 0x1A1A2E);
        } else if ("compact".equals(key) || key.startsWith("show_")) {
            rebuild(); // Layout changes (hidden rows in compact mode)
        }
    }

    private void applyTextColor(int color) {
        if (fpsLabel       != null) fpsLabel.color(color);
        if (coordsLabel    != null) coordsLabel.color(color);
        if (chunkLabel     != null) chunkLabel.color(color);
        if (biomeLabel     != null) biomeLabel.color(color);
        if (directionLabel != null) directionLabel.color(color);
        if (lightLabel     != null) lightLabel.color(color);
        if (memoryLabel    != null) memoryLabel.color(color);
    }

    @Override
    protected void onFrame(float partialTicks, long deltaNanos) {
        Minecraft mc = Minecraft.getMinecraft();
        EntityPlayer player = mc.player;
        if (player == null || mc.world == null) return;

        heavyUpdateTimer += deltaNanos;

        // FPS — every frame, with adaptive color
        if (fpsLabel != null && getSetting("show_fps", true)) {
            int fps = Minecraft.getDebugFPS();
            fpsLabel.text("FPS: " + fps);
            fpsLabel.color(fps >= 60 ? 0xFF55FF55 : fps >= 30 ? 0xFFFFFF55 : 0xFFFF5555);
        }

        // Coordinates — every frame for maximum smoothness
        if (coordsLabel != null && getSetting("show_coords", true)) {
            boolean compact = getSetting("compact", false);
            coordsLabel.text(compact
                ? String.format("XYZ: %.0f / %.0f / %.0f", player.posX, player.posY, player.posZ)
                : String.format("X: %.1f  Y: %.1f  Z: %.1f", player.posX, player.posY, player.posZ));
        }

        // Chunk — every frame
        if (chunkLabel != null && getSetting("show_chunk", true)) {
            int cx = MathHelper.floor(player.posX) >> 4;
            int cz = MathHelper.floor(player.posZ) >> 4;
            chunkLabel.text("Chunk: " + cx + ", " + cz);
        }

        // Direction — every frame (interpolated rotation)
        if (directionLabel != null && getSetting("show_direction", true)) {
            float yaw = MathHelper.wrapDegrees(player.rotationYaw);
            directionLabel.text("Direction: " + getCardinalDirection(yaw)
                + " (" + String.format("%.0f", yaw) + ")");
        }

        // Heavy data — every 500ms via nanotime timer
        if (heavyUpdateTimer >= HEAVY_UPDATE_INTERVAL) {
            heavyUpdateTimer = 0L;
            BlockPos pos = player.getPosition();

            if (getSetting("show_biome", true)) {
                cachedBiome = mc.world.getBiome(pos).getBiomeName();
            }
            if (getSetting("show_light", true)) {
                Chunk chunk = mc.world.getChunk(pos);
                cachedLight = "Block: "
                    + chunk.getLightFor(net.minecraft.world.EnumSkyBlock.BLOCK, pos)
                    + "  Sky: "
                    + chunk.getLightFor(net.minecraft.world.EnumSkyBlock.SKY, pos);
            }
            if (getSetting("show_memory", true)) {
                Runtime rt = Runtime.getRuntime();
                long used = (rt.totalMemory() - rt.freeMemory()) / 1048576L;
                long max  = rt.maxMemory() / 1048576L;
                cachedMemory = used + "MB / " + max + "MB (" + (used * 100 / max) + "%)";
            }
        }

        // Apply cached values for heavy data
        if (biomeLabel   != null) biomeLabel.text("Biome: " + cachedBiome);
        if (lightLabel   != null) lightLabel.text("Light " + cachedLight);
        if (memoryLabel  != null) memoryLabel.text("Mem: " + cachedMemory);

        // Visibility per setting
        if (fpsLabel       != null) fpsLabel.visible(getSetting("show_fps", true));
        if (coordsLabel    != null) coordsLabel.visible(getSetting("show_coords", true));
        if (chunkLabel     != null) chunkLabel.visible(getSetting("show_chunk", true));
        if (biomeLabel     != null) biomeLabel.visible(getSetting("show_biome", true));
        if (directionLabel != null) directionLabel.visible(getSetting("show_direction", true));
        if (lightLabel     != null) lightLabel.visible(getSetting("show_light", true));
        if (memoryLabel    != null) memoryLabel.visible(getSetting("show_memory", true));
    }

    @Override
    protected void onTick() {
        // All update logic is in onFrame() for smoothness.
        // onTick() is not used here but could serve for network calls.
    }

    private static String getCardinalDirection(float yaw) {
        if (yaw < 0) yaw += 360f;
        if (yaw >= 337.5f || yaw < 22.5f)  return "S";
        if (yaw < 67.5f)                    return "SW";
        if (yaw < 112.5f)                   return "W";
        if (yaw < 157.5f)                   return "NW";
        if (yaw < 202.5f)                   return "N";
        if (yaw < 247.5f)                   return "NE";
        if (yaw < 292.5f)                   return "E";
        return "SE";
    }
}

Key takeaways from this example

nanotime timer for heavy data

Accessing the biome (world.getBiome()), chunk light and JVM memory are non-trivial operations. Executing them every frame (60+ times/s) would waste CPU. The heavyUpdateTimer limits them to once every 500ms, while labels are updated from the cache every frame — rendering stays smooth without overhead.

rebuild() on layout settings

When the compact setting or a show_* toggle changes, we call rebuild() rather than simply changing label visibility. In compact mode, the text format itself changes ("XYZ: 10 / 64 / -30" instead of "X: 10.0 Y: 64.0 Z: -30.0"), which requires a complete component rebuild. If only visibility changed, a simple label.visible(false) would suffice without rebuild().

Overlay editor: auto-snap alignment system v1.2.0

When you open the in-game editor (GuiOverlayHub) and drag an overlay, the auto-snap system activates. The overlay being moved automatically snaps to the edges and centers of other overlays and to fixed screen reference points, enabling precise alignment without entering coordinates manually.

All coordinates are expressed in 1920x1080 design pixels, consistent with the rest of the EriAPI positioning system.

Snap types

The system detects 10 distinct snap types. A snap triggers when the distance between two reference points is less than or equal to 5 design pixels.

Type Description Source reference Target reference
LEFT_TO_LEFT Left edge aligned to the left edge of another overlay x of the dragged overlay x of a neighbor overlay
LEFT_TO_RIGHT Left edge aligned to the right edge of another overlay x of the dragged overlay x + width of a neighbor overlay
RIGHT_TO_RIGHT Right edge aligned to the right edge of another overlay x + width of the dragged overlay x + width of a neighbor overlay
RIGHT_TO_LEFT Right edge aligned to the left edge of another overlay x + width of the dragged overlay x of a neighbor overlay
TOP_TO_TOP Top edge aligned to the top edge of another overlay y of the dragged overlay y of a neighbor overlay
TOP_TO_BOTTOM Top edge aligned to the bottom edge of another overlay y of the dragged overlay y + height of a neighbor overlay
BOTTOM_TO_BOTTOM Bottom edge aligned to the bottom edge of another overlay y + height of the dragged overlay y + height of a neighbor overlay
BOTTOM_TO_TOP Bottom edge aligned to the top edge of another overlay y + height of the dragged overlay y of a neighbor overlay
CENTER_H Horizontal center aligned to the horizontal center of another overlay or the screen center (960) x + width/2 of the dragged overlay horizontal center of the target
CENTER_V Vertical center aligned to the vertical center of another overlay or the screen center (540) y + height/2 of the dragged overlay vertical center of the target

Screen edge snapping

In addition to other overlays, all four screen borders are snap targets:

  • Left border — x = 0
  • Right border — x = 1920
  • Top border — y = 0
  • Bottom border — y = 1080

The screen center (960, 540) is also a target for CENTER_H and CENTER_V snaps.

Visual feedback

Two visual indicators appear during dragging to guide positioning:

Guide lines (cyan)

When at least one snap is active, a semi-transparent cyan guide line appears on the snap axis:

  • A vertical line for a snap on the X axis (left/right edges or horizontal center).
  • A horizontal line for a snap on the Y axis (top/bottom edges or vertical center).

These lines span the full width or height of the screen to remain visible regardless of the overlay position. They disappear immediately when the overlay is released or when the snap is no longer active.

Overlap indicator (red)

If the dragged overlay overlaps (at least partially) another overlay during the drag, it takes on a semi-transparent red tint. This signals that an overlap is occurring. The overlay can still be dropped at that position — this is a visual warning, not a block.

Bypassing snap (Shift key)

To move an overlay freely without any snapping, hold the Shift key during the drag. The overlay will then follow the mouse movement exactly, pixel by pixel, with no snapping or position correction applied.

Usage tip

Use normal drag (without Shift) to align overlays with each other or with screen borders. Use Shift only when you need a very specific position that does not correspond to any grid reference point.