Merge pull request 'Add GUI scale and glass, and rework the title screen' (#8) from glass-blur into master

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-09-28 20:01:19 +00:00
22 changed files with 348 additions and 122 deletions
@@ -6,6 +6,7 @@ import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.FloatProperty;
import org.saturnclient.config.property.IntProperty;
import org.saturnclient.config.property.KeybindingProperty;
import org.saturnclient.config.property.Property;
@@ -18,9 +19,30 @@ public class Config {
public static BoolProperty saturnTitleScreen = Property.bool(true);
public static BoolProperty cloakPhysics = Property.bool(true);
public static BoolProperty stagger = Property.bool(true);
/** The world or panorama behind Saturn's screens, faded in as they open */
public static IntProperty backgroundBlur = Property.integer(5).range(0, 10);
/** Glass behind translucent menus (panels), and widgets (buttons, the dock) */
public static IntProperty menuBlur = Property.integer(8).range(0, 20);
public static IntProperty widgetBlur = Property.integer(6).range(0, 20);
// How big Saturn's screens are, relative to Minecraft's GUI scale
public static FloatProperty titleScale = guiScale();
public static FloatProperty menuScale = guiScale();
public static FloatProperty hudMenuScale = guiScale();
public static KeybindingProperty openEmoteWheel = Property.keybinding(GLFWProvider.GLFW_KEY_B);
private static FloatProperty guiScale() {
return Property.floatProp(1f).range(0.5f, 2f, 0.05f).unit("×");
}
/**
* Whether any glass can be drawn: a glass blur is on, and a background it
* goes behind isn't solid.
*/
public static boolean usesGlass() {
boolean translucent = Theme.getPanelBg() >>> 24 < 0xFF || Theme.PRIMARY.value >>> 24 < 0xFF;
return translucent && (menuBlur.value > 0 || widgetBlur.value > 0);
}
public static IdentifierRef getLogo() {
return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO;
}
@@ -34,7 +56,12 @@ public class Config {
config.property("Open Emote Wheel", openEmoteWheel, "The key that opens the emote wheel");
config.property("Cloak Physics", cloakPhysics, "Let cloaks swing as you move");
config.property("Stagger Animations", stagger, "Animate list items one after another");
config.property("Background blur", backgroundBlur, "How much the game behind Saturn's screens is blurred");
config.property("World blur", backgroundBlur, "How much the world or panorama behind Saturn's screens is blurred");
config.property("Menu blur", menuBlur, "How much the game is blurred through translucent menus");
config.property("Widget blur", widgetBlur, "How much the game is blurred through translucent buttons and the dock");
config.property("Title screen scale", titleScale, "How big the title screen is, relative to Minecraft's GUI scale");
config.property("Menu scale", menuScale, "How big the mods, settings and cosmetics menus are, relative to Minecraft's GUI scale");
config.property("HUD menu scale", hudMenuScale, "How big the emote wheel and shift menu are, relative to Minecraft's GUI scale");
// Initialize a sub namespace for theme
Theme.init(config);
@@ -10,14 +10,14 @@ import org.saturnclient.config.property.SelectProperty;
public class Theme {
public static ConfigManager config;
// Main UI background (slightly warm dark to match Saturn tone)
public static final ColorProperty BACKGROUND = Property.color(0xFF12100D);
// Main UI background (slightly warm dark to match Saturn tone), translucent over the world
public static final ColorProperty BACKGROUND = Property.color(0xD112100D);
// Default text color
public static final ColorProperty FOREGROUND = Property.color(0xFFF2E6D2);
// Widget default background (normal state)
public static final ColorProperty PRIMARY = Property.color(0xFF1C1915);
// Widget default background (normal state), translucent over the world
public static final ColorProperty PRIMARY = Property.color(0xB81C1915);
// Widget text color
public static final ColorProperty PRIMARY_FG = Property.color(0xFFEAD9B8);
@@ -62,10 +62,10 @@ public class Theme {
}
public static final List<Preset> PRESETS = List.of(
new Preset("Saturn", 0xFF12100D, 0xFFF2E6D2, 0xFF1C1915, 0xFFEAD9B8, 0xFFE2BF7D, 0xFF1A140A, 0xFFE2BF7D),
new Preset("Midnight", 0xFF0E1117, 0xFFE4EAF5, 0xFF171C26, 0xFFC9D4E8, 0xFF8FB3FF, 0xFF0A1224, 0xFF8FB3FF),
new Preset("Amethyst", 0xFF120F17, 0xFFEDE4F7, 0xFF1C1724, 0xFFD8C9EB, 0xFFC79BFF, 0xFF1A0F2A, 0xFFC79BFF),
new Preset("Emerald", 0xFF0D1210, 0xFFE2F2EA, 0xFF16201B, 0xFFC4E0D2, 0xFF7FD1A8, 0xFF0A1F15, 0xFF7FD1A8));
new Preset("Saturn", 0xD112100D, 0xFFF2E6D2, 0xB81C1915, 0xFFEAD9B8, 0xFFE2BF7D, 0xFF1A140A, 0xFFE2BF7D),
new Preset("Midnight", 0xD10E1117, 0xFFE4EAF5, 0xB8171C26, 0xFFC9D4E8, 0xFF8FB3FF, 0xFF0A1224, 0xFF8FB3FF),
new Preset("Amethyst", 0xD1120F17, 0xFFEDE4F7, 0xB81C1724, 0xFFD8C9EB, 0xFFC79BFF, 0xFF1A0F2A, 0xFFC79BFF),
new Preset("Emerald", 0xD10D1210, 0xFFE2F2EA, 0xB816201B, 0xFFC4E0D2, 0xFF7FD1A8, 0xFF0A1F15, 0xFF7FD1A8));
public static void init(ConfigManager parent) {
config = new ConfigManager(parent, "UI Style");
@@ -98,10 +98,19 @@ public class Theme {
}
/**
* Translucent background for panels drawn over the world
* Background for panels drawn over the world, as translucent as the
* background color is
*/
public static int getPanelBg() {
return withAlpha(0.82f, BACKGROUND.value);
return BACKGROUND.value;
}
/**
* Background for popups, which sit over other panels: the background color,
* kept nearly solid so what's under it doesn't show through
*/
public static int getPopupBg() {
return withAlpha(0.97f, BACKGROUND.value | 0xFF000000);
}
/**
@@ -26,6 +26,11 @@ public class ElementRenderer {
for (Element element : elements) {
element.playAnimationFrame(elapsed);
// Not faded in yet
if (element.opacity <= 0) {
continue;
}
renderScope.getMatrixStack().push();
renderScope.setOpacity(element.opacity);
renderScope.getMatrixStack().translate(element.x, element.y);
@@ -34,6 +34,14 @@ public interface RenderScope {
if (color == 0)
return;
drawTexture(getRoundedRectangle(width, height, radius), x, y, 0, 0, width, height, color);
}
/**
* The white rounded rectangle that {@link #drawRoundedRectangle} tints, as a
* cached texture.
*/
default IdentifierRef getRoundedRectangle(int width, int height, int radius) {
radius = Math.min(radius, width / 2);
radius = Math.min(radius, height / 2);
@@ -42,7 +50,38 @@ public interface RenderScope {
width, height,
radius, radius);
drawSvgShape(rect, "rounded_" + width + "_" + height + "_" + radius, x, y, width, height, color);
return getSvgShape(rect, "rounded_" + width + "_" + height + "_" + radius, width, height);
}
/**
* Frosts a rounded rectangle for a translucent background to be drawn over:
* the game behind the screen, blurred by {@code blur}, cut to exactly the
* shape {@link #drawRoundedRectangle} draws. Render scopes without glass
* draw nothing.
*/
default void drawGlass(int x, int y, int width, int height, int radius, int blur) {
drawGlass(getRoundedRectangle(width, height, radius), x, y, width, height, blur);
}
/**
* Draws the game behind the screen, blurred by {@code blur}, where
* {@code mask}, stretched over the area, is opaque.
*/
default void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
}
/**
* A {@link #drawRoundedRectangle} with a border, frosted with glass blurred
* by {@code blur} when the background isn't solid. A blur of 0 leaves it
* plain, showing the screen's own background blur.
*/
default void drawGlassRectangle(int x, int y, int width, int height, int radius, int blur, int thickness,
int background, int border) {
if (blur > 0 && background >>> 24 < 0xFF) {
drawGlass(x, y, width, height, radius, blur);
}
drawRoundedRectangle(x, y, width, height, radius, thickness, background, border);
}
/**
@@ -89,15 +128,22 @@ public interface RenderScope {
* {@code color}.
*/
private void drawSvgShape(String shape, String cacheKey, int x, int y, int width, int height, int color) {
drawTexture(getSvgShape(shape, cacheKey, width, height), x, y, 0, 0, width, height, color);
}
/**
* {@code shape} (SVG elements in a {@code width} x {@code height} canvas,
* drawn in white) rendered to a cached texture.
*/
private IdentifierRef getSvgShape(String shape, String cacheKey, int width, int height) {
String svg = String.format(
"<svg xmlns='http://www.w3.org/2000/svg' width='%d' height='%d'>%s</svg>",
width, height, shape);
InputStream svgStream = new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8));
drawTexture(SvgTexture.getSvg(svgStream,
IdentifierRef.ofSaturn("textures/gui/tmp_rect/" + cacheKey),
width * 4, height * 4), x, y, 0, 0, width, height, color);
return SvgTexture.getSvg(svgStream, IdentifierRef.ofSaturn("textures/gui/tmp_rect/" + cacheKey),
width * 4, height * 4);
}
void drawBorder(int x, int y, int width, int height, int color);
@@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.List;
import org.saturnclient.config.Config;
import org.saturnclient.config.property.FloatProperty;
public abstract class SaturnScreen {
/** How long the background blur takes to fade in, in ms */
@@ -14,8 +15,10 @@ public abstract class SaturnScreen {
public static interface ScreenProvider {
public void close();
/** The screen's width in GUI pixels */
public int getWidth();
/** The screen's height in GUI pixels */
public int getHeight();
}
@@ -23,6 +26,10 @@ public abstract class SaturnScreen {
public int width;
public int height;
public String title;
/** GUI pixels per Saturn unit, from {@link #getScale()} when the screen was built */
public float scale = 0.5f;
/** Whether a mouse button is held, so the screen isn't rebuilt under a drag */
private boolean mouseDown = false;
// Saturn Screen
public List<Element> elements = new ArrayList<>();
@@ -51,10 +58,39 @@ public abstract class SaturnScreen {
return Math.round(Config.backgroundBlur.value * Math.min(elapsed / (float) BLUR_FADE_IN, 1f));
}
/**
* The GUI scale setting this screen follows; the menu scale unless the
* screen is another kind.
*/
protected FloatProperty getScaleSetting() {
return Config.menuScale;
}
/**
* GUI pixels per Saturn unit. At 1× a Saturn unit is half a GUI pixel.
* Enlarging stops where the screen would be smaller than 320x240 GUI
* pixels, the least Minecraft's own GUI scale leaves.
*/
public float getScale() {
float fit = Math.min(provider.getWidth() / 320f, provider.getHeight() / 240f);
return 0.5f * Math.min(getScaleSetting().value, Math.max(1f, fit));
}
/**
* Rebuilds the screen if its scale setting changed, once no drag is going
* on (the slider being dragged would be replaced).
*/
public void updateScale() {
if (!mouseDown && getScale() != scale) {
init();
}
}
public void init() {
elements.clear();
width = provider.getWidth();
height = provider.getHeight();
scale = getScale();
width = (int) (provider.getWidth() / scale);
height = (int) (provider.getHeight() / scale);
Popup.reset(width, height);
ui(); // abstraction to render the saturn ui and also render extra stuff here
}
@@ -72,7 +108,7 @@ public abstract class SaturnScreen {
public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) {
renderScope.getMatrixStack().push();
renderScope.getMatrixStack().scale(0.5f, 0.5f);
renderScope.getMatrixStack().scale(scale, scale);
ElementRenderer.render(new ArrayList<>(elements), elapsed, renderScope, mouseX, mouseY);
Popup.render(renderScope, elapsed, mouseX, mouseY);
@@ -85,8 +121,9 @@ public abstract class SaturnScreen {
// --------------
public boolean mouseClicked(double mouseX, double mouseY, int button) {
mouseX *= 2;
mouseY *= 2;
mouseDown = true;
mouseX /= scale;
mouseY /= scale;
if (!Popup.mouseClicked(mouseX, mouseY, button)) {
ElementRenderer.mouseClicked(elements, mouseX, mouseY, button);
@@ -96,8 +133,8 @@ public abstract class SaturnScreen {
}
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
mouseX *= 2;
mouseY *= 2;
mouseX /= scale;
mouseY /= scale;
if (!Popup.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) {
ElementRenderer.mouseDragged(elements, mouseX, mouseY, button, deltaX, deltaY);
@@ -107,8 +144,9 @@ public abstract class SaturnScreen {
}
public boolean mouseReleased(double mouseX, double mouseY, int button) {
mouseX *= 2;
mouseY *= 2;
mouseDown = false;
mouseX /= scale;
mouseY /= scale;
// Everything hears the release, so no element is left thinking it's being dragged.
Popup.mouseReleased(mouseX, mouseY, button);
@@ -122,8 +160,8 @@ public abstract class SaturnScreen {
double mouseY,
double horizontalAmount,
double verticalAmount) {
mouseX *= 2;
mouseY *= 2;
mouseX /= scale;
mouseY /= scale;
if (!Popup.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
ElementRenderer.mouseScrolled(elements, mouseX, mouseY, horizontalAmount, verticalAmount);
@@ -16,12 +16,12 @@ public class Fade extends Animation {
@Override
public void tick(double progress, Element element) {
element.opacity = Math.max(0.1f, targetOpacity * (float) progress);
element.opacity = targetOpacity * (float) progress;
}
@Override
public void init(Element element) {
targetOpacity = element.opacity;
element.opacity = 0.1f;
element.opacity = 0;
}
}
@@ -3,6 +3,7 @@ package org.saturnclient.ui.components;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Config;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
@@ -15,12 +16,13 @@ import org.saturnclient.ui.resources.Fonts;
* while connected to Saturn.
*/
public class AccountChip extends Element {
public static final int HEIGHT = 40;
public static final int HEIGHT = 32;
private static final int HEAD = 28;
private static final int PADDING = 6;
private static final int DOT = 7;
private static final float NAME_SCALE = 0.7f;
private static final int HEAD = 18;
private static final int PADDING = 7;
private static final int GAP = 8;
private static final int DOT = 6;
private static final float NAME_SCALE = 0.55f;
private static final int CONNECTED = 0xFF7FD1A8;
private final String name;
@@ -29,13 +31,14 @@ public class AccountChip extends Element {
String username = Providers.saturn.getClient().getUsername();
this.name = username != null ? username : "Player";
this.height = HEIGHT;
this.width = PADDING + HEAD + 10 + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + 10 + DOT + 12;
this.width = PADDING + HEAD + GAP + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + GAP + DOT
+ GAP + 2;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1, Theme.getPanelBg(),
Theme.getBorder());
renderScope.drawGlassRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Config.widgetBlur.value, 1,
Theme.getPanelBg(), Theme.getBorder());
// The face, then the hat layer over it, from the 64x64 skin
IdentifierRef skin = Providers.saturn.getPlayerSkin();
@@ -43,11 +46,11 @@ public class AccountChip extends Element {
renderScope.drawTexture(skin, PADDING, headY, 8, 8, HEAD, HEAD, 8, 8, 64, 64);
renderScope.drawTexture(skin, PADDING, headY, 40, 8, HEAD, HEAD, 8, 8, 64, 64);
int nameX = PADDING + HEAD + 10;
int nameX = PADDING + HEAD + GAP;
renderScope.drawText(NAME_SCALE, name, nameX, Field.textY(height, NAME_SCALE), Theme.FONT.value,
Theme.FOREGROUND.value);
int dotX = nameX + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + 10;
int dotX = nameX + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + GAP;
renderScope.drawRoundedRectangle(dotX, (height - DOT) / 2, DOT, DOT, DOT / 2,
ServiceClient.isConnected() ? CONNECTED : Theme.withAlpha(0.3f, Theme.FOREGROUND.value));
}
@@ -60,7 +60,7 @@ public class CosmeticTile extends Element {
int border = isEquipped ? Theme.ACCENT.value
: Theme.withAlpha(hovered ? 0.22f : 0.06f, Theme.FOREGROUND.value);
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, isEquipped ? 2 : 1,
Theme.withAlpha(0.72f, Theme.PRIMARY.value), border);
Theme.PRIMARY.value, border);
// Preview, fitted into its area
int areaWidth = width - INSET * 2;
@@ -3,6 +3,7 @@ package org.saturnclient.ui.components;
import java.util.List;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Config;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
@@ -15,12 +16,12 @@ public class Dock extends Element {
public record Item(IdentifierRef icon, Runnable onClick) {
}
public static final int HEIGHT = 52;
public static final int HEIGHT = 40;
private static final int PADDING = 6;
private static final int PADDING = 5;
private static final int ITEM = HEIGHT - PADDING * 2;
private static final int ICON = 18;
private static final int GAP = 4;
private static final int ICON = 15;
private static final int GAP = 3;
private final List<Item> items;
@@ -32,7 +33,8 @@ public class Dock extends Element {
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, 1, Theme.getPanelBg(),
renderScope.drawGlassRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, Config.widgetBlur.value, 1,
Theme.getPanelBg(),
Theme.withAlpha(0.1f, Theme.FOREGROUND.value));
for (int i = 0; i < items.size(); i++) {
@@ -41,7 +41,7 @@ public class EmoteTile extends Element {
boolean hovered = ctx.isHovering();
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, hovered ? 2 : 1,
Theme.withAlpha(0.72f, Theme.PRIMARY.value),
Theme.PRIMARY.value,
hovered ? Theme.ACCENT.value : Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
int imageSize = height - INSET * 2 - NAME_AREA;
@@ -46,7 +46,7 @@ public class SaturnModule extends Element {
// Card
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
Theme.withAlpha(0.72f, Theme.PRIMARY.value), border);
Theme.PRIMARY.value, border);
if (enabled) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
@@ -38,7 +38,7 @@ public class ThemePresetCard extends Element {
: Theme.withAlpha(ctx.isHovering() ? 0.22f : 0.06f, Theme.FOREGROUND.value);
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, active ? 2 : 1,
Theme.withAlpha(0.72f, Theme.PRIMARY.value), border);
Theme.PRIMARY.value, border);
// A panel in the preset's colors: a line of text over an accent and a widget
int previewWidth = width - PADDING * 2;
@@ -1,7 +1,10 @@
package org.saturnclient.ui.components.inputs;
import java.awt.Color;
import java.awt.image.BufferedImage;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Theme;
import org.saturnclient.config.property.ColorProperty;
import org.saturnclient.ui.Element;
@@ -27,6 +30,18 @@ public class ColorPicker extends Element {
private static final int SQUARE_Y = OPACITY_Y + SLIDER_HEIGHT + GAP;
private static final int HUE_Y = SQUARE_Y + SIZE + GAP;
// Gradient strips, made once and stretched over each part, so a frame is a
// few draws instead of one per pixel. Tinting the white ones colors them.
private static final int STRIP = 256;
/** White fading out to the right: saturation, over the hue */
private static final IdentifierRef SATURATION = IdentifierRef.ofSaturn("textures/gui/color_picker/saturation");
/** Black fading in downwards: brightness, over the saturation */
private static final IdentifierRef BRIGHTNESS = IdentifierRef.ofSaturn("textures/gui/color_picker/brightness");
/** White fading in to the right, tinted with the color */
private static final IdentifierRef OPACITY = IdentifierRef.ofSaturn("textures/gui/color_picker/opacity");
private static final IdentifierRef HUE = IdentifierRef.ofSaturn("textures/gui/color_picker/hue");
private static boolean stripsRegistered = false;
private final ColorProperty prop;
private float hue;
@@ -46,8 +61,10 @@ public class ColorPicker extends Element {
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
registerStrips();
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
Theme.withAlpha(0.97f, Theme.BACKGROUND.value), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
Theme.getPopupBg(), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
renderOpacity(renderScope);
renderSquare(renderScope);
@@ -55,22 +72,16 @@ public class ColorPicker extends Element {
}
private void renderOpacity(RenderScope renderScope) {
int rgb = prop.value & 0xFFFFFF;
for (int x = 0; x < SIZE; x++) {
int alpha = (int) (x / (float) SIZE * 255) & 0xFF;
renderScope.fill(PADDING + x, OPACITY_Y, PADDING + x + 1, OPACITY_Y + SLIDER_HEIGHT, (alpha << 24) | rgb);
}
renderScope.drawTexture(OPACITY, PADDING, OPACITY_Y, 0, 0, SIZE, SLIDER_HEIGHT,
0xFF000000 | prop.value);
drawMarker(renderScope, PADDING + (int) (opacity * SIZE), OPACITY_Y);
}
private void renderSquare(RenderScope renderScope) {
for (int x = 0; x < SIZE; x++) {
for (int y = 0; y < SIZE; y++) {
int color = Color.HSBtoRGB(hue, x / (float) SIZE, 1f - y / (float) SIZE);
renderScope.fill(PADDING + x, SQUARE_Y + y, PADDING + x + 1, SQUARE_Y + y + 1, 0xFF000000 | color);
}
}
renderScope.fill(PADDING, SQUARE_Y, PADDING + SIZE, SQUARE_Y + SIZE, 0xFF000000 | Color.HSBtoRGB(hue, 1f, 1f));
renderScope.drawTexture(SATURATION, PADDING, SQUARE_Y, 0, 0, SIZE, SIZE, 0xFFFFFFFF);
renderScope.drawTexture(BRIGHTNESS, PADDING, SQUARE_Y, 0, 0, SIZE, SIZE, 0xFFFFFFFF);
int selX = PADDING + (int) (saturation * SIZE) - 4;
int selY = SQUARE_Y + (int) ((1f - brightness) * SIZE) - 4;
@@ -79,14 +90,36 @@ public class ColorPicker extends Element {
}
private void renderHue(RenderScope renderScope) {
for (int x = 0; x < SIZE; x++) {
int color = Color.HSBtoRGB(x / (float) SIZE, 1f, 1f);
renderScope.fill(PADDING + x, HUE_Y, PADDING + x + 1, HUE_Y + SLIDER_HEIGHT, 0xFF000000 | color);
}
renderScope.drawTexture(HUE, PADDING, HUE_Y, 0, 0, SIZE, SLIDER_HEIGHT, 0xFFFFFFFF);
drawMarker(renderScope, PADDING + (int) (hue * SIZE), HUE_Y);
}
private static void registerStrips() {
if (stripsRegistered) {
return;
}
BufferedImage saturation = new BufferedImage(STRIP, 1, BufferedImage.TYPE_INT_ARGB);
BufferedImage brightness = new BufferedImage(1, STRIP, BufferedImage.TYPE_INT_ARGB);
BufferedImage opacity = new BufferedImage(STRIP, 1, BufferedImage.TYPE_INT_ARGB);
BufferedImage hue = new BufferedImage(STRIP, 1, BufferedImage.TYPE_INT_ARGB);
for (int i = 0; i < STRIP; i++) {
int alpha = i * 255 / (STRIP - 1);
saturation.setRGB(i, 0, ((255 - alpha) << 24) | 0xFFFFFF);
brightness.setRGB(0, i, alpha << 24);
opacity.setRGB(i, 0, (alpha << 24) | 0xFFFFFF);
hue.setRGB(i, 0, 0xFF000000 | Color.HSBtoRGB(i / (float) STRIP, 1f, 1f));
}
Providers.saturn.registerBufferedImageTexture(SATURATION, saturation);
Providers.saturn.registerBufferedImageTexture(BRIGHTNESS, brightness);
Providers.saturn.registerBufferedImageTexture(OPACITY, opacity);
Providers.saturn.registerBufferedImageTexture(HUE, hue);
stripsRegistered = true;
}
private static void drawMarker(RenderScope renderScope, int x, int sliderY) {
renderScope.fill(x - 2, sliderY - 2, x + 2, sliderY + SLIDER_HEIGHT + 2, 0xFFFFFFFF);
renderScope.fill(x - 1, sliderY - 1, x + 1, sliderY + SLIDER_HEIGHT + 1, 0xFF000000);
@@ -38,7 +38,7 @@ public class CurvePreview extends Element {
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
Theme.withAlpha(0.97f, Theme.BACKGROUND.value), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
Theme.getPopupBg(), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
renderScope.drawText(TITLE_SCALE, "PREVIEW", PADDING, PADDING - 2, Theme.FONT.value, Theme.getMutedFg());
@@ -45,7 +45,7 @@ public class OptionList extends Element {
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
Theme.withAlpha(0.97f, Theme.BACKGROUND.value), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
Theme.getPopupBg(), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
int visible = Math.min(options.length, MAX_VISIBLE);
hovered = getIndexAt(ctx.mouseX, ctx.mouseY);
@@ -13,6 +13,8 @@ import org.saturnclient.ui.RenderScope;
public class AnimationStagger extends Element {
protected List<Element> children = new ArrayList<>();
public int delay;
/** When the first child starts, in ms */
public int start = 0;
public AnimationStagger(int delay) {
this.delay = delay;
@@ -22,13 +24,21 @@ public class AnimationStagger extends Element {
this.delay = config.stagger.value;
}
/**
* Holds the children back until {@code start} ms after the screen opens.
*/
public AnimationStagger after(int start) {
this.start = start;
return this;
}
public void draw(Element element) {
if (!Config.stagger.value) {
element.animation = null;
}
if (element.animation != null) {
element.animation.delay = delay * children.size();
element.animation.delay = start + delay * children.size();
}
ElementRenderer.draw(children, element);
@@ -1,5 +1,6 @@
package org.saturnclient.ui.elements;
import org.saturnclient.config.Config;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
@@ -38,8 +39,8 @@ public class Button extends Element {
* The button's surface, shared with {@link IconButton}.
*/
static void drawSurface(RenderScope renderScope, int width, int height, boolean hovered) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
hovered ? Theme.withAlpha(0.9f, Theme.PRIMARY.value) : Theme.getPanelBg(),
renderScope.drawGlassRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Config.widgetBlur.value, 1,
hovered ? Theme.PRIMARY.value : Theme.getPanelBg(),
Theme.withAlpha(hovered ? 0.25f : 0.1f, Theme.FOREGROUND.value));
}
@@ -1,12 +1,14 @@
package org.saturnclient.ui.elements;
import org.saturnclient.config.Config;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
/**
* The translucent, outlined surface that screens lay their content on.
* The translucent, outlined surface that screens lay their content on,
* frosted over the game where glass is supported.
*/
public class Panel extends Element {
@@ -16,7 +18,7 @@ public class Panel extends Element {
}
public static void drawBackground(RenderScope renderScope, int width, int height) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.BG_RADIUS.value, 1,
renderScope.drawGlassRectangle(0, 0, width, height, Theme.BG_RADIUS.value, Config.menuBlur.value, 1,
Theme.getPanelBg(), Theme.getBorder());
}
}
@@ -7,6 +7,7 @@ import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Config;
import org.saturnclient.config.property.FloatProperty;
import org.saturnclient.config.Theme;
import org.saturnclient.cosmetics.Emotes;
import org.saturnclient.ui.Element;
@@ -43,6 +44,11 @@ public class EmoteWheel extends SaturnScreen {
super("Emote Wheel");
}
@Override
protected FloatProperty getScaleSetting() {
return Config.hudMenuScale;
}
@Override
public void ui() {
List<String> emotes = Emotes.availableEmotes;
@@ -125,8 +131,8 @@ public class EmoteWheel extends SaturnScreen {
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
Theme.getPanelBg(), Theme.getBorder());
renderScope.drawGlassRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Config.widgetBlur.value,
1, Theme.getPanelBg(), Theme.getBorder());
float titleScale = 0.54f;
drawCentered(renderScope, "EMOTES", titleScale, pages > 1 ? 24 : (height - 10) / 2, Theme.getMutedFg());
@@ -18,8 +18,8 @@ import org.saturnclient.ui.resources.Fonts;
* Moves and resizes the enabled HUD mods, drawn as they are in game. Drag one
* to move it, or drag its corner handle to resize it.
*
* Unlike other Saturn screens this works in GUI pixels, like the HUD itself:
* it draws without the half scale and takes the mouse unscaled.
* Unlike other Saturn screens this works in GUI pixels, like the HUD itself,
* whatever Saturn's GUI scale.
*/
public class HudEditor extends SaturnScreen {
private static final String HINT = "Drag to move · Drag the corner to resize · Esc when done";
@@ -45,6 +45,11 @@ public class HudEditor extends SaturnScreen {
}
}
@Override
public float getScale() {
return 1f;
}
@Override
public void ui() {
// Placing HUDs needs a clear view of the game.
@@ -102,10 +107,7 @@ public class HudEditor extends SaturnScreen {
@Override
public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) {
// The screen hands over the mouse in Saturn units, twice GUI pixels.
int guiMouseX = mouseX / 2;
int guiMouseY = mouseY / 2;
HudMod hovered = draggingMod != null ? draggingMod : getModAt(guiMouseX, guiMouseY);
HudMod hovered = draggingMod != null ? draggingMod : getModAt(mouseX, mouseY);
for (HudMod mod : hudMods) {
ModLayout dim = mod.getDimensions();
@@ -149,12 +151,11 @@ public class HudEditor extends SaturnScreen {
}
private void drawHint(RenderScope renderScope) {
int guiWidth = width / 2;
int textWidth = (int) (Fonts.getWidth(HINT, Theme.getBodyFont()) * HINT_SCALE);
int textHeight = (int) (Fonts.getHeight() * HINT_SCALE);
int pillWidth = textWidth + 20;
int pillHeight = textHeight + 10;
int x = (guiWidth - pillWidth) / 2;
int x = (width - pillWidth) / 2;
int y = 8;
renderScope.drawRoundedRectangle(x, y, pillWidth, pillHeight, pillHeight / 2, 1, Theme.getPanelBg(),
@@ -176,13 +177,11 @@ public class HudEditor extends SaturnScreen {
}
private void keepOnScreen(ModLayout dim) {
int guiWidth = width / 2;
int guiHeight = height / 2;
int w = right(dim) - dim.x.value;
int h = bottom(dim) - dim.y.value;
dim.x.value = Math.max(0, Math.min(dim.x.value, guiWidth - w));
dim.y.value = Math.max(0, Math.min(dim.y.value, guiHeight - h));
dim.x.value = Math.max(0, Math.min(dim.x.value, width - w));
dim.y.value = Math.max(0, Math.min(dim.y.value, height - h));
}
private static int right(ModLayout dim) {
@@ -3,6 +3,7 @@ package org.saturnclient.ui.screens;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Config;
import org.saturnclient.config.property.FloatProperty;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideY;
@@ -24,6 +25,11 @@ public class ShiftMenu extends SaturnScreen {
super("Shift Menu");
}
@Override
protected FloatProperty getScaleSetting() {
return Config.hudMenuScale;
}
@Override
public void ui() {
draw(new ImageTexture(Textures.LOGO_TEXT)
@@ -6,9 +6,11 @@ import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.game.MinecraftClientRef.MinecraftScreen;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Config;
import org.saturnclient.config.property.FloatProperty;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.anim.Animation;
import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideFade;
import org.saturnclient.ui.anim.SlideY;
@@ -16,96 +18,133 @@ import org.saturnclient.ui.components.AccountChip;
import org.saturnclient.ui.components.Dock;
import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.Button;
import org.saturnclient.ui.elements.IconButton;
import org.saturnclient.ui.elements.ImageTexture;
import org.saturnclient.ui.elements.Label;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures;
/**
* Saturn's title screen: the logo over the game's menus in the middle, and
* along the bottom the player, a dock of Saturn's screens and the version.
* Saturn's title screen: the logo over the game's menus in the middle, the
* player and a quit button in the top corners, and along the bottom a dock of
* Saturn's screens, the shift menu and Minecraft's options, and the version.
*/
public class TitleMenu extends SaturnScreen {
private static final int LOGO = 88;
private static final int WORDMARK_WIDTH = 200;
private static final int WORDMARK_HEIGHT = 20;
// The logo and wordmark as the shift menu has them: the wordmark over the logo's foot
private static final int LOGO = 98;
private static final int WORDMARK_WIDTH = 98;
private static final int WORDMARK_HEIGHT = 10;
private static final int WORDMARK_Y = 88;
/** From the logo's foot to the menus, close so they read as one block */
private static final int MENUS_GAP = 14;
private static final int COLUMN_WIDTH = 280;
private static final int BUTTON_HEIGHT = 36;
private static final int GAP = 8;
private static final int EDGE = 28;
private static final float VERSION_SCALE = 0.62f;
/** Space around the account chip, quit button, dock and version, from the screen's edges */
private static final int EDGE = 14;
/** The top corners sit a little tighter */
private static final int TOP_EDGE = 10;
/** As tall as the account chip beside it */
private static final int QUIT_SIZE = AccountChip.HEIGHT;
// The intro comes in in reading order, left to right and top to bottom, one step apart
private static final int STEP_ACCOUNT = 0;
private static final int STEP_QUIT = 1;
private static final int STEP_LOGO = 2;
private static final int STEP_MENUS = 3;
/** After the two menus */
private static final int STEP_VERSION = 5;
private static final int STEP_DOCK = 6;
private static final float VERSION_SCALE = 0.5f;
private static final float VERSION_ALPHA = 0.6f;
public TitleMenu() {
super("Title Menu");
}
@Override
protected FloatProperty getScaleSetting() {
return Config.titleScale;
}
@Override
public void ui() {
blurBackground = false;
// Logo and menus, centered together a little above the middle
int menusHeight = BUTTON_HEIGHT * 3 + GAP * 2;
int blockHeight = LOGO + 16 + WORDMARK_HEIGHT + 36 + menusHeight;
int menusHeight = BUTTON_HEIGHT * 2 + GAP;
int blockHeight = LOGO + MENUS_GAP + menusHeight;
int top = (height - blockHeight) / 2 - 20;
draw(new ImageTexture(Config.getLogo())
.dimensions(LOGO, LOGO)
.position((width - LOGO) / 2, top)
.animation(new SlideY(AnimationConfig.logo, -20)));
draw(new ImageTexture(Textures.LOGO_TEXT_BIG)
.animation(step(new SlideY(AnimationConfig.logo, -20), STEP_LOGO)));
draw(new ImageTexture(Textures.LOGO_TEXT)
.dimensions(WORDMARK_WIDTH, WORDMARK_HEIGHT)
.position((width - WORDMARK_WIDTH) / 2, top + LOGO + 16)
.animation(new Fade(AnimationConfig.logo.duration.value)));
.position((width - WORDMARK_WIDTH) / 2, top + WORDMARK_Y)
.animation(step(new Fade(AnimationConfig.logo.duration.value), STEP_LOGO)));
int rowHeight = BUTTON_HEIGHT + GAP;
int halfWidth = (COLUMN_WIDTH - GAP) / 2;
AnimationStagger menus = new AnimationStagger(AnimationConfig.mainMenu);
AnimationStagger menus = new AnimationStagger(AnimationConfig.mainMenu)
.after(stepDelay(STEP_MENUS));
addMenuButton(menus, new Button("Singleplayer",
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.SelectWorld))
.dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, 0));
addMenuButton(menus, new Button("Multiplayer",
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Multiplayer))
.dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, rowHeight));
addMenuButton(menus, new Button("Options",
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Options))
.dimensions(halfWidth, BUTTON_HEIGHT).position(0, rowHeight * 2));
addMenuButton(menus, new Button("Quit",
() -> Providers.saturn.getClient().scheduleStop())
.dimensions(halfWidth, BUTTON_HEIGHT).position(COLUMN_WIDTH - halfWidth, rowHeight * 2));
.dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, BUTTON_HEIGHT + GAP));
draw(menus.position((width - COLUMN_WIDTH) / 2, top + LOGO + 16 + WORDMARK_HEIGHT + 36));
draw(menus.position((width - COLUMN_WIDTH) / 2, top + LOGO + MENUS_GAP));
// Along the bottom: the player, Saturn's screens, the version
// Top corners: the player, and quitting the game
draw(new AccountChip()
.position(EDGE, height - EDGE - AccountChip.HEIGHT)
.animation(new Fade(AnimationConfig.mainMenu)));
.position(TOP_EDGE, TOP_EDGE)
.animation(step(new SlideFade(AnimationConfig.mainMenu, 8), STEP_ACCOUNT)));
draw(new IconButton(Textures.ICON_CLOSE, () -> Providers.saturn.getClient().scheduleStop())
.dimensions(QUIT_SIZE, QUIT_SIZE)
.position(width - TOP_EDGE - QUIT_SIZE, TOP_EDGE)
.animation(step(new SlideFade(AnimationConfig.mainMenu, 8), STEP_QUIT)));
// Along the bottom: the dock of Saturn's screens, the shift menu and Minecraft's options, and the version
Dock dock = new Dock(List.of(
new Dock.Item(Textures.ICON_MODS, () -> Providers.saturn.getClient().setScreen(new ModMenu())),
new Dock.Item(Textures.ICON_COSMETICS,
() -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())),
new Dock.Item(Textures.ICON_HUD_EDITOR,
() -> Providers.saturn.getClient().setScreen(new HudEditor()))));
new Dock.Item(Textures.LOGO, () -> Providers.saturn.getClient().setScreen(new ShiftMenu())),
new Dock.Item(Textures.ICON_SETTINGS,
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Options))));
draw(dock.position((width - dock.width) / 2, height - EDGE - Dock.HEIGHT)
.animation(new SlideFade(AnimationConfig.mainMenu, 12)));
.animation(step(new SlideFade(AnimationConfig.mainMenu, -12), STEP_DOCK)));
String version = getVersionText();
int versionWidth = (int) (Fonts.getWidth(version, Theme.getBodyFont()) * VERSION_SCALE);
int versionHeight = (int) (Fonts.getHeight() * VERSION_SCALE);
draw(new Label(() -> version, VERSION_SCALE, Theme::getBodyFont, Theme::getMutedFg)
.position(width - EDGE - versionWidth, height - EDGE - (AccountChip.HEIGHT + versionHeight) / 2)
.animation(new Fade(AnimationConfig.mainMenu)));
draw(new Label(() -> version, VERSION_SCALE, Theme::getBodyFont,
() -> Theme.withAlpha(VERSION_ALPHA, Theme.getMutedFg()))
.position(EDGE, height - EDGE - versionHeight)
.animation(step(new Fade(AnimationConfig.mainMenu), STEP_VERSION)));
}
/**
* "Saturn Client 0.1.0 · Minecraft 1.21.11", from the mod version
* "0.1.0+1.21.11".
* When the intro's {@code step} starts, in ms: the Main Menu stagger apart,
* or all at once when staggering is off.
*/
private static int stepDelay(int step) {
return Config.stagger.value ? step * AnimationConfig.mainMenu.stagger.value : 0;
}
private static Animation step(Animation animation, int step) {
animation.delay = stepDelay(step);
return animation;
}
/**
* "Saturn 0.1.0 · 1.21.11", from the mod version "0.1.0+1.21.11".
*/
private static String getVersionText() {
String[] version = Providers.saturn.getModVersion().split("\\+", 2);
String text = "Saturn Client " + version[0];
return version.length > 1 ? text + " · Minecraft " + version[1] : text;
String text = "Saturn " + version[0];
return version.length > 1 ? text + " · " + version[1] : text;
}
private static void addMenuButton(AnimationStagger stagger, Element button) {