Skip to content

Commit 404d8f1

Browse files
HammerGSclaude
andcommitted
Address review feedback and make the splash tag's colour and angle configurable
Review fixes: - Correct a stale @PARAM on deriveSplashTagFont, which still documented the panelWidth parameter the method had before it was changed to take the logo's bounds. Flagged by CodeQL, the code-quality bot and Copilot. - Measure the tag by the lettering actually painted rather than by the font's ascent and descent. Copilot was right that ascent minus descent underestimates the rendered height: for this font at 100pt it gives 69 where the painted lettering of "Core Rules Edition" is 83 tall. The height cap was therefore letting a tag grow past the wordmark plate. Using ascent plus descent would swing the other way and overestimate at 119, so both the cap and the centring now use the glyphs' visual bounds, which is what actually lands on the screen. The shipping tag is bound by width rather than height at every window size, so its appearance does not change; a short tag such as "BETA" is the case this was getting wrong. New settings, both optional: - splashTagColor takes the same colour names as notesColor, so the tag's colour is set the same way as the menu note's. Absent, or not a name MegaMek knows, leaves it red. - splashTagRotation sets the angle in degrees. Zero is level, a negative angle drops the left-hand end and lifts the right, and a positive angle does the opposite. Absent leaves it at -2.5 as before, and an angle beyond -45 to 45 falls back to that and says so in megamek.log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 03174ef commit 404d8f1

3 files changed

Lines changed: 148 additions & 24 deletions

File tree

megamek/resources/Version.properties

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,18 @@ notesFontSize=14
8080
# and a warning goes to megamek.log.
8181
#
8282
splashTag=Core Rules Edition
83+
#
84+
# Colour of the sprayed tag, from the same list of names as notesColor above: red, orange, yellow,
85+
# green, blue, cyan, magenta, pink, white, black, gray, lightgray, darkgray (grey is accepted too).
86+
# Leave it out and the tag is sprayed in red. An unrecognised name falls back to red and is reported
87+
# in megamek.log. Whichever colour is chosen is left slightly see-through, so the logo shows through
88+
# the paint.
89+
#
90+
#splashTagColor=red
91+
#
92+
# Angle the tag is sprayed at, in degrees. Zero is level. A NEGATIVE angle drops the left-hand end and
93+
# lifts the right-hand end; a POSITIVE angle does the opposite. Leave it out and the tag sits at -2.5,
94+
# just off level, which is enough to stop it reading as a tidy caption. Anything beyond -45 to 45 is
95+
# treated as a mistake, falls back to -2.5, and is reported in megamek.log.
96+
#
97+
#splashTagRotation=-2.5

megamek/src/megamek/Version.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,23 @@ public final class Version implements Comparable<Version>, Serializable {
9191
/** Key of the optional tag sprayed across the MegaMek logo on the splash art. Absent for ordinary releases. */
9292
private static final String SPLASH_TAG_BUNDLE_KEY = "splashTag";
9393

94+
/** Key of the optional colour name the splash tag is sprayed in. Absent unless a splash tag sets one. */
95+
private static final String SPLASH_TAG_COLOR_BUNDLE_KEY = "splashTagColor";
96+
97+
/** Key of the optional angle, in degrees, the splash tag is sprayed at. Absent unless a splash tag sets one. */
98+
private static final String SPLASH_TAG_ROTATION_BUNDLE_KEY = "splashTagRotation";
99+
100+
/** The angle the splash tag is sprayed at when it does not ask for one: very slightly off level. */
101+
public static final double DEFAULT_SPLASH_TAG_ROTATION = -2.5;
102+
103+
/**
104+
* Bounds accepted for a configured splash tag angle. Anything past this is treated as a mistake rather than
105+
* an intention, since a tag turned much further than this stops reading as paint on a sign and starts
106+
* running off the artwork.
107+
*/
108+
private static final double MINIMUM_SPLASH_TAG_ROTATION = -45.0;
109+
private static final double MAXIMUM_SPLASH_TAG_ROTATION = 45.0;
110+
94111
/**
95112
* The value {@link #getBuildNotesFontSize()} reports when no point size is configured, meaning the build note
96113
* is drawn at whatever size the rest of the menu uses.
@@ -118,6 +135,8 @@ public final class Version implements Comparable<Version>, Serializable {
118135
private static final String BUILD_NOTES_COLOR_NAME = readOptionalEntryFromBundle(NOTES_COLOR_BUNDLE_KEY);
119136
private static final int BUILD_NOTES_FONT_SIZE = readOptionalNotesFontSizeFromBundle();
120137
private static final String SPLASH_TAG = readOptionalEntryFromBundle(SPLASH_TAG_BUNDLE_KEY);
138+
private static final String SPLASH_TAG_COLOR_NAME = readOptionalEntryFromBundle(SPLASH_TAG_COLOR_BUNDLE_KEY);
139+
private static final double SPLASH_TAG_ROTATION = readOptionalSplashTagRotationFromBundle();
121140

122141
private int major;
123142
private int minor;
@@ -322,6 +341,36 @@ private static int readOptionalNotesFontSizeFromBundle() {
322341
return parsedFontSize;
323342
}
324343

344+
/**
345+
* Reads the optional {@code splashTagRotation} entry from {@code Version.properties}.
346+
*
347+
* @return the configured angle in degrees, or {@link #DEFAULT_SPLASH_TAG_ROTATION} when the key is absent,
348+
* blank, not a number, or turned further than makes sense on the artwork
349+
*/
350+
private static double readOptionalSplashTagRotationFromBundle() {
351+
final String configuredRotation = readOptionalEntryFromBundle(SPLASH_TAG_ROTATION_BUNDLE_KEY);
352+
if (configuredRotation == null) {
353+
return DEFAULT_SPLASH_TAG_ROTATION;
354+
}
355+
356+
final double parsedRotation = MathUtility.parseDouble(configuredRotation, Double.NaN);
357+
final boolean isNotANumber = Double.isNaN(parsedRotation);
358+
final boolean isTurnedTooFar = (parsedRotation < MINIMUM_SPLASH_TAG_ROTATION)
359+
|| (parsedRotation > MAXIMUM_SPLASH_TAG_ROTATION);
360+
if (isNotANumber || isTurnedTooFar) {
361+
LOGGER.warn("[Version] Version.properties sets splashTagRotation to '{}', which is not an angle "
362+
+ "between {} and {} degrees. The splash tag will be sprayed at {} degrees instead.",
363+
configuredRotation,
364+
MINIMUM_SPLASH_TAG_ROTATION,
365+
MAXIMUM_SPLASH_TAG_ROTATION,
366+
DEFAULT_SPLASH_TAG_ROTATION);
367+
return DEFAULT_SPLASH_TAG_ROTATION;
368+
}
369+
370+
LOGGER.debug("[Version] Splash tag angle {} degrees loaded from Version.properties", parsedRotation);
371+
return parsedRotation;
372+
}
373+
325374
/**
326375
* Reads an optional entry from {@code Version.properties}, treating an absent key and a blank value alike.
327376
*
@@ -423,6 +472,30 @@ public static boolean hasSplashTag() {
423472
return SPLASH_TAG != null;
424473
}
425474

475+
/**
476+
* Returns the colour the splash tag asks to be sprayed in, as the plain name written in
477+
* {@code Version.properties} rather than as a colour object, for the same reason as
478+
* {@link #getBuildNotesColorName()}: this class is also used well away from the user interface.
479+
*
480+
* @return the configured colour name, or {@code null} when the tag did not ask for a particular colour
481+
*/
482+
public static @Nullable String getSplashTagColorName() {
483+
return SPLASH_TAG_COLOR_NAME;
484+
}
485+
486+
/**
487+
* Returns the angle, in degrees, the splash tag is sprayed at.
488+
*
489+
* <p>Zero is level. A negative angle drops the left-hand end and lifts the right-hand end; a positive angle
490+
* does the opposite. This follows the screen's own sense of rotation, where the vertical axis points
491+
* downwards.</p>
492+
*
493+
* @return the configured angle, or {@link #DEFAULT_SPLASH_TAG_ROTATION} when the tag did not ask for one
494+
*/
495+
public static double getSplashTagRotation() {
496+
return SPLASH_TAG_ROTATION;
497+
}
498+
426499
// region Getters
427500
public int getMajor() {
428501
return major;

megamek/src/megamek/client/ui/clientGUI/MegaMekGUI.java

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import java.awt.event.WindowAdapter;
4343
import java.awt.event.WindowEvent;
4444
import java.awt.font.GlyphVector;
45+
import java.awt.geom.Rectangle2D;
4546
import java.awt.image.VolatileImage;
4647
import java.io.File;
4748
import java.io.FileInputStream;
@@ -164,8 +165,11 @@ public class MegaMekGUI implements IPreferenceChangeListener {
164165
/** Family name of the spray-paint font the splash tag is drawn in. Ships in mm-data under data/fonts. */
165166
private static final String SPRAY_PAINT_FONT_FAMILY = "Rubik Spray Paint";
166167

167-
/** Colour of the sprayed splash tag: a strong red, left see-through enough that the logo shows through it. */
168-
private static final Color SPLASH_TAG_COLOR = new Color(198, 26, 26, 232);
168+
/** Colour the splash tag is sprayed in when it does not name one: a strong red. */
169+
private static final Color SPLASH_TAG_DEFAULT_COLOR = new Color(198, 26, 26);
170+
171+
/** How solid the sprayed tag is, out of 255. Short of opaque, so the logo shows through the paint. */
172+
private static final int SPLASH_TAG_ALPHA = 232;
169173

170174
/** Colour outlining the sprayed tag, which lifts the red clear of the light wordmark underneath it. */
171175
private static final Color SPLASH_TAG_OUTLINE_COLOR = new Color(0, 0, 0, 225);
@@ -176,9 +180,6 @@ public class MegaMekGUI implements IPreferenceChangeListener {
176180
*/
177181
private static final double SPLASH_TAG_OUTLINE_WIDTH_FRACTION = 0.085;
178182

179-
/** How far off level the sprayed tag sits, in degrees, so it does not read as a tidy interface caption. */
180-
private static final double SPLASH_TAG_TILT_DEGREES = -2.5;
181-
182183
/** Fraction of the logo's width the sprayed tag spans, leaving it a little narrower than the logo itself. */
183184
private static final double SPLASH_TAG_WIDTH_FRACTION_OF_LOGO = 0.92;
184185

@@ -218,6 +219,8 @@ public class MegaMekGUI implements IPreferenceChangeListener {
218219
private ManagedVolatileImage medalImage;
219220
/** Font the splash tag is sprayed in, or {@code null} when this build has no tag to spray. */
220221
private Font splashTagFont;
222+
/** Colour the splash tag is sprayed in, worked out once when the menu is built. */
223+
private Color splashTagColor = SPLASH_TAG_DEFAULT_COLOR;
221224
private TipOfTheDay tipOfTheDay;
222225
private AbstractRandomArmyDialog randomArmyDialog;
223226
private NetworkInformationDialog networkInformationDialog;
@@ -435,6 +438,7 @@ private void showMainMenu() {
435438
logoImage = new ManagedVolatileImage(getImage(FILENAME_LOGO), Transparency.TRANSLUCENT);
436439
medalImage = new ManagedVolatileImage(getImage(FILENAME_MEDAL), Transparency.TRANSLUCENT);
437440
splashTagFont = loadSplashTagFont();
441+
splashTagColor = resolveSplashTagColor();
438442
Dimension splashPanelPreferredSize = calculateSplashPanelPreferredSize(scaledMonitorSize, splashImage);
439443
// This is an empty panel that will contain the splash image
440444
// Draw background, border, and children first
@@ -540,7 +544,7 @@ private void showMainMenu() {
540544
* is why {@code Version.properties} asks for a short one.</p>
541545
*
542546
* @param graphics the graphics context the tag will be painted with, used to measure the lettering
543-
* @param panelWidth the width in pixels of the splash artwork
547+
* @param logoBounds the area the logo occupies, which the tag is sized against
544548
*
545549
* @return the font to spray the tag in, already at the right size, or {@code null} when this build has no tag
546550
*/
@@ -551,11 +555,14 @@ private void showMainMenu() {
551555

552556
String splashTag = Version.getSplashTag();
553557

554-
// Measure at a reference size and scale from there, which is one measurement instead of a search.
558+
// Measure at a reference size and scale from there, which is one measurement instead of a search. The
559+
// measurement is of the painted lettering itself rather than of the font's ascent and descent, because
560+
// those describe what the font could draw rather than what this particular tag does draw.
555561
Font referenceFont = splashTagFont.deriveFont(SPLASH_TAG_REFERENCE_FONT_SIZE);
556-
FontMetrics referenceMetrics = graphics.getFontMetrics(referenceFont);
557-
int referenceWidth = referenceMetrics.stringWidth(splashTag);
558-
int referenceHeight = referenceMetrics.getAscent() - referenceMetrics.getDescent();
562+
Rectangle2D referenceInk = referenceFont.createGlyphVector(graphics.getFontRenderContext(), splashTag)
563+
.getVisualBounds();
564+
double referenceWidth = referenceInk.getWidth();
565+
double referenceHeight = referenceInk.getHeight();
559566
if ((referenceWidth <= 0) || (referenceHeight <= 0)) {
560567
return null;
561568
}
@@ -597,28 +604,30 @@ private void drawSplashTag(Graphics2D graphics, @Nullable Rectangle logoBounds)
597604
}
598605

599606
String splashTag = Version.getSplashTag();
600-
FontMetrics tagMetrics = graphics.getFontMetrics(sizedSplashTagFont);
601-
int tagWidth = tagMetrics.stringWidth(splashTag);
602-
603-
// Centred across the logo, and down it at the height the wordmark sits at
604-
int tagX = logoBounds.x + ((logoBounds.width - tagWidth) / 2);
605-
int lettersHeight = tagMetrics.getAscent() - tagMetrics.getDescent();
606-
int wordmarkCentreY = logoBounds.y + (int) (logoBounds.height * LOGO_WORDMARK_CENTRE_FRACTION);
607-
int tagY = wordmarkCentreY + (lettersHeight / 2);
608607

609608
Graphics2D tagGraphics = (Graphics2D) graphics.create();
610609
try {
611610
tagGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
612611
tagGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
613612
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
614-
tagGraphics.rotate(Math.toRadians(SPLASH_TAG_TILT_DEGREES),
615-
tagX + (tagWidth / 2.0),
616-
tagY - (tagMetrics.getAscent() / 2.0));
617613

618-
// The lettering is outlined in black and then filled in red, rather than simply drawn, so that it
619-
// stands clear of the pale wordmark it is sprayed across.
614+
// Measure the painted lettering, so that centring it uses where the paint actually lands rather than
615+
// where the font says its tallest and lowest characters could reach.
620616
GlyphVector tagGlyphs = sizedSplashTagFont.createGlyphVector(tagGraphics.getFontRenderContext(),
621617
splashTag);
618+
Rectangle2D tagInk = tagGlyphs.getVisualBounds();
619+
620+
// Centred across the logo, and down it at the height the wordmark sits at
621+
int tagX = logoBounds.x + (int) ((logoBounds.width - tagInk.getWidth()) / 2 - tagInk.getX());
622+
int wordmarkCentreY = logoBounds.y + (int) (logoBounds.height * LOGO_WORDMARK_CENTRE_FRACTION);
623+
int tagY = wordmarkCentreY - (int) (tagInk.getY() + (tagInk.getHeight() / 2));
624+
625+
tagGraphics.rotate(Math.toRadians(Version.getSplashTagRotation()),
626+
tagX + tagInk.getCenterX(),
627+
wordmarkCentreY);
628+
629+
// The lettering is outlined and then filled, rather than simply drawn, so that it stands clear of the
630+
// pale wordmark it is sprayed across.
622631
Shape tagOutline = tagGlyphs.getOutline(tagX, tagY);
623632

624633
float outlineWidth = (float) Math.max(1.0,
@@ -627,13 +636,40 @@ private void drawSplashTag(Graphics2D graphics, @Nullable Rectangle logoBounds)
627636
tagGraphics.setColor(SPLASH_TAG_OUTLINE_COLOR);
628637
tagGraphics.draw(tagOutline);
629638

630-
tagGraphics.setColor(SPLASH_TAG_COLOR);
639+
tagGraphics.setColor(splashTagColor);
631640
tagGraphics.fill(tagOutline);
632641
} finally {
633642
tagGraphics.dispose();
634643
}
635644
}
636645

646+
/**
647+
* Works out what colour to spray the splash tag in, the same way the build note's colour is chosen: by the
648+
* plain colour name written in {@code Version.properties}.
649+
*
650+
* <p>A tag that names no colour, or names one that is not recognised, is sprayed in red. Whichever colour is
651+
* used is left slightly see-through so the logo shows through the paint.</p>
652+
*
653+
* @return the colour to spray the tag in, never {@code null}
654+
*/
655+
private static Color resolveSplashTagColor() {
656+
String configuredColorName = Version.getSplashTagColorName();
657+
if (configuredColorName == null) {
658+
LOGGER.debug("[SplashTag] No splashTagColor set - the tag is sprayed in red");
659+
return UIUtil.addAlpha(SPLASH_TAG_DEFAULT_COLOR, SPLASH_TAG_ALPHA);
660+
}
661+
662+
Color namedColor = colorForName(configuredColorName);
663+
if (namedColor == null) {
664+
LOGGER.warn("[SplashTag] Version.properties sets splashTagColor to '{}', which is not a colour name "
665+
+ "MegaMek knows. The tag will be sprayed in red instead.", configuredColorName);
666+
return UIUtil.addAlpha(SPLASH_TAG_DEFAULT_COLOR, SPLASH_TAG_ALPHA);
667+
}
668+
669+
LOGGER.debug("[SplashTag] Splash tag colour set to {}", configuredColorName);
670+
return UIUtil.addAlpha(namedColor, SPLASH_TAG_ALPHA);
671+
}
672+
637673
/**
638674
* Loads the font the splash tag is sprayed in, once, when the menu is built.
639675
*

0 commit comments

Comments
 (0)