A universal graphics API for your Minecraft plugin.

A Paper plugin paired with a Fabric client mod. Draw shapes, text and images on a player's screen from your own plugin.

int id = GraphicsAPI.drawRectangle(player, x, y, w, h,
    true, 0xBF2A2A2A, false, true, 8,
    GraphicsAPI.NO_GROUP, screenId);
draggable = true

Quick start

Installed once on the server as its own plugin. Your plugin depends on it, it doesn't bundle a copy.

1. Add the dependency

<repositories>
  <repository>
    <id>spoofies-private-repo</id>
    <url>https://repo.spoofies.dev/repo</url>
  </repository>
</repositories>

<dependency>
  <groupId>the.spoofies</groupId>
  <artifactId>graphicapi</artifactId>
  <version>1.0</version>
  <scope>provided</scope>
</dependency>

2. Declare it in plugin.yml

depend: [GraphicsAPI]

3. Draw something

int id = GraphicsAPI.drawRectangle(player, 40, 30, 160, 80, true);

A filled rectangle at (40, 30), 160×80, on the HUD.

Elements

Every shape you draw is an element with an id, returned from the drawX(...) call. Reuse the id to update or remove it:

int id = GraphicsAPI.drawRectangle(player, 40, 30, 160, 80, true);

GraphicsAPI.updateRectangle(player, id, 50, 30, 160, 80,
    true, 0xBF333232, false, false, 0, GraphicsAPI.NO_GROUP);

GraphicsAPI.remove(player, id);

Elements persist until removed or the player disconnects. Redraw only when something changes.

Groups

Tag elements with a shared groupId. They move together when the group's anchor is dragged:

int square = GraphicsAPI.drawRectangle(player, x, y, w, h, true,
    color, false, true, 6, GraphicsAPI.NO_GROUP, screenId);

GraphicsAPI.drawText(player, x + 10, y - 12, color, false, 1.0f, 0,
    "Ready", false, square, screenId);

Screens

Elements sit on the HUD by default: visible, no cursor. Open a screen to make things clickable and draggable:

GraphicsAPI.openScreen(player, MY_SCREEN_ID);

A real Minecraft Screen. Cursor appears, Esc closes it. Elements tagged with MY_SCREEN_ID (or unscoped, NO_SCREEN) render inside it. Everything else stays hidden.

Buttons and controls inside a screen are elements with interactive = true and an onClick handler.

Move / Scale / Grid

Move

draggable = true at creation. Dragging the body moves it.

GraphicsAPI.drawRectangle(player, x, y, w, h, true, color,
    false, true, 8, GraphicsAPI.NO_GROUP, screenId);

Scale

Toggled after creation. Corner handles appear on the element. Drag one to resize from that corner.

GraphicsAPI.setScalable(player, elementId, true);

setDraggable(player, elementId, boolean) works the same way for drag.

Grid snap

GraphicsAPI.setGridSnap(player, true);

Shows a grid overlay. Move and resize snap to it while dragging. Wire it to your own button or command.

Shapes

Five element types, each with matching draw / update / animate methods.

Rectangle
GraphicsAPI.drawRectangle(player, x, y, width, height, filled,
    argbColor, interactive, draggable, cornerRadius, groupId, screenId);
ParameterTypeDescription
playerPlayerWho sees this.
x, yintTop-left corner, pixels.
width, heightintSize, pixels.
filledbooleanSolid fill vs outline only.
argbColorint0xAARRGGBB.
interactivebooleanClickable while a screen is open.
draggablebooleanBody can be dragged.
cornerRadiusint0 for sharp corners, higher rounds them.
groupIdintAnother element's id, or NO_GROUP.
screenIdintWhich screen, or NO_SCREEN for unscoped.
Text
GraphicsAPI.drawText(player, x, y, argbColor, shadow, scale,
    wrapWidth, text, interactive, groupId, screenId);
ParameterTypeDescription
playerPlayerWho sees this.
x, yintTop-left of the text.
argbColorint0xAARRGGBB.
shadowbooleanStandard drop-shadow.
scalefloat1.0 is default size.
wrapWidthint0 for single line, else word-wrap width in pixels.
textStringContent.
interactivebooleanClickable while a screen is open.
groupIdintAnother element's id, or NO_GROUP.
screenIdintWhich screen, or NO_SCREEN.
Line
GraphicsAPI.drawLine(player, x1, y1, x2, y2, thickness, argbColor, groupId, screenId);
ParameterTypeDescription
playerPlayerWho sees this.
x1, y1intStart point.
x2, y2intEnd point.
thicknessintWidth, pixels.
argbColorint0xAARRGGBB.
groupIdintAnother element's id, or NO_GROUP.
screenIdintWhich screen, or NO_SCREEN.

No interactive or draggable options. Lines don't support either.

Circle
GraphicsAPI.drawCircle(player, centerX, centerY, radius, filled,
    argbColor, interactive, groupId, screenId);
ParameterTypeDescription
playerPlayerWho sees this.
centerX, centerYintCenter point.
radiusintPixels.
filledbooleanSolid disc vs ring outline.
argbColorint0xAARRGGBB.
interactivebooleanClickable while a screen is open.
groupIdintAnother element's id, or NO_GROUP.
screenIdintWhich screen, or NO_SCREEN.
Image
GraphicsAPI.sendImageFile(player, "banner.png");
GraphicsAPI.drawImage(player, x, y, width, height, imageId, interactive, groupId, screenId);
ParameterTypeDescription
playerPlayerWho sees this.
x, yintTop-left corner.
width, heightintDraw size.
imageIdStringMust match a name already sent via sendImageFile.
interactivebooleanClickable while a screen is open.
groupIdintAnother element's id, or NO_GROUP.
screenIdintWhich screen, or NO_SCREEN.

See Images for how the transfer works.

Interactivity

Set interactive = true and register a handler. Fires only while a screen is open:

int button = GraphicsAPI.drawRectangle(player, x, y, w, h, true,
    color, true, false, 4, groupId, screenId);

GraphicsAPI.onClick(button, (p, click) -> {
    p.sendMessage("Clicked at " + click.mouseX() + "," + click.mouseY());
});

Find out where a drag or resize ended up:

GraphicsAPI.onPanelMoved(anchorId, (p, newX, newY) -> { /* ... */ });
GraphicsAPI.onElementResized(anchorId, (p, newWidth, newHeight) -> { /* ... */ });

Screen & Hud builder

For screen-scoped controls or a persistent HUD group, build declaratively instead of hand-tracking ids and cleanup.

Screen

A method that builds and returns one:

private Screen buildEditScreen(MyState state) {
    Screen screen = GraphicsAPI.screen(MY_SCREEN_ID);

    state.buttonRef = screen.rectangle(x, y, w, h, true, color)
            .interactive()
            .onClick((p, click) -> onButtonClick(p));
    state.labelRef = screen.text(x + 8, y + 5, "Label");

    return screen;
}

GraphicsAPI.openScreen(player, buildEditScreen(state));
state.buttonId = state.buttonRef.id(); // valid only after openScreen runs

Everything drawn through it is removed automatically when the player closes the screen.

Hud

Same shape, persistent. Multiple different huds can be enabled at once:

private Hud buildMyHud(Player player, MyState state) {
    Hud hud = GraphicsAPI.hud(MY_HUD_ID);
    ElementRef box = hud.rectangle(x, y, 40, 40, true, color).draggable();
    hud.text(x, y - 12, "Label").groupWith(box);
    return hud;
}

GraphicsAPI.enableHud(player, buildMyHud(player, state));
GraphicsAPI.disableHud(player, MY_HUD_ID);

What it's not for

Content that changes every tick, like a countdown. Build once with the builder, keep the id, use updateText/updateRectangle for the parts that change.

Animation

Every shape has a matching animateX(...), interpolated client-side every render frame.

Rectangle

GraphicsAPI.animateRectangle(player, id, toX, toY, toWidth, toHeight,
    filled, toArgbColor, interactive, draggable, cornerRadius, durationMs, easing);

Text

GraphicsAPI.animateText(player, id, toX, toY, toArgbColor, shadow,
    toScale, wrapWidth, text, interactive, durationMs, easing);

Line

GraphicsAPI.animateLine(player, id, toX1, toY1, toX2, toY2,
    thickness, toArgbColor, durationMs, easing);

Circle

GraphicsAPI.animateCircle(player, id, toCenterX, toCenterY, toRadius,
    filled, toArgbColor, interactive, durationMs, easing);

Image

GraphicsAPI.animateImage(player, id, toX, toY, toWidth, toHeight,
    imageId, interactive, durationMs, easing);

Every one takes the same trailing pair: durationMs (int) and easing. Values: Easing.LINEAR, Easing.EASE_OUT_CUBIC, Easing.EASE_IN_OUT_CUBIC.

Images

GraphicsAPI.sendImageFile(player, "banner.png");
GraphicsAPI.drawImage(player, x, y, width, height, "banner");

PNGs are chunked and streamed to the client, then decoded into a texture. drawImage can be called right after sending. Shows a placeholder until the transfer finishes, then swaps in automatically. Re-sending the same name replaces it.