mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:40:43 +00:00
Add white glow behind hovered player's name
The hovered player (tile owner under the cursor, already tracked via uHighlightOwnerID for cull bypass) now gets a soft white glow behind their name. The glow is derived from the MSDF distance field: a white band past the outline with quadratic falloff, composited behind the glyph and clamped to the SDF margin so it never clips at quad edges. Glow size and strength are tunable via hoverGlowWidth/hoverGlowAlpha in render-settings.json, exposed as sliders in the graphics settings modal (persisted as graphics overrides) and in the debug GUI. Includes schema and apply tests for the new override fields, covering the 0 edge case (0 disables the glow, not "unset").
This commit is contained in:
@@ -944,6 +944,10 @@
|
||||
"name_cull_desc": "Hide names smaller than this size",
|
||||
"hover_fade_label": "Name opacity under cursor",
|
||||
"hover_fade_desc": "How visible names are while your cursor is over them (1 to disable fading)",
|
||||
"hover_glow_width_label": "Hover glow size",
|
||||
"hover_glow_width_desc": "How far the white glow extends behind the hovered player's name",
|
||||
"hover_glow_alpha_label": "Hover glow strength",
|
||||
"hover_glow_alpha_desc": "How bright the white glow behind the hovered player's name is (0 to disable)",
|
||||
"colored_names_label": "Name color",
|
||||
"colored_names_desc": "Show player names in their player color or in black",
|
||||
"colored": "Colored",
|
||||
|
||||
@@ -24,6 +24,14 @@ const HOVER_FADE_MIN = 0;
|
||||
const HOVER_FADE_MAX = 1;
|
||||
const HOVER_FADE_STEP = 0.05;
|
||||
|
||||
const HOVER_GLOW_WIDTH_MIN = 0;
|
||||
const HOVER_GLOW_WIDTH_MAX = 8;
|
||||
const HOVER_GLOW_WIDTH_STEP = 0.5;
|
||||
|
||||
const HOVER_GLOW_ALPHA_MIN = 0;
|
||||
const HOVER_GLOW_ALPHA_MAX = 1;
|
||||
const HOVER_GLOW_ALPHA_STEP = 0.05;
|
||||
|
||||
const HIGHLIGHT_FILL_MIN = 0;
|
||||
const HIGHLIGHT_FILL_MAX = 1;
|
||||
const HIGHLIGHT_FILL_STEP = 0.01;
|
||||
@@ -159,6 +167,20 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
);
|
||||
}
|
||||
|
||||
private currentHoverGlowWidth(): number {
|
||||
return (
|
||||
this.userSettings.graphicsOverrides().name?.hoverGlowWidth ??
|
||||
renderDefaults.name.hoverGlowWidth
|
||||
);
|
||||
}
|
||||
|
||||
private currentHoverGlowAlpha(): number {
|
||||
return (
|
||||
this.userSettings.graphicsOverrides().name?.hoverGlowAlpha ??
|
||||
renderDefaults.name.hoverGlowAlpha
|
||||
);
|
||||
}
|
||||
|
||||
private patchName(patch: Partial<GraphicsOverrides["name"]>) {
|
||||
const current = this.userSettings.graphicsOverrides();
|
||||
this.userSettings.setGraphicsOverrides({
|
||||
@@ -349,6 +371,16 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
this.patchName({ hoverFadeAlpha: value });
|
||||
}
|
||||
|
||||
private onHoverGlowWidthChange(event: Event) {
|
||||
const value = parseFloat((event.target as HTMLInputElement).value);
|
||||
this.patchName({ hoverGlowWidth: value });
|
||||
}
|
||||
|
||||
private onHoverGlowAlphaChange(event: Event) {
|
||||
const value = parseFloat((event.target as HTMLInputElement).value);
|
||||
this.patchName({ hoverGlowAlpha: value });
|
||||
}
|
||||
|
||||
private currentDarkNames(): boolean {
|
||||
return (
|
||||
this.userSettings.graphicsOverrides().name?.darkNames ??
|
||||
@@ -371,6 +403,8 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
const nameScale = this.currentNameScale();
|
||||
const nameCull = this.currentNameCull();
|
||||
const hoverFade = this.currentHoverFade();
|
||||
const hoverGlowWidth = this.currentHoverGlowWidth();
|
||||
const hoverGlowAlpha = this.currentHoverGlowAlpha();
|
||||
const namesColored = !this.currentDarkNames();
|
||||
const classicIcons = this.currentClassicIcons();
|
||||
const highlightFill = this.currentHighlightFill();
|
||||
@@ -492,6 +526,56 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("graphics_setting.hover_glow_width_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("graphics_setting.hover_glow_width_desc")}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min=${HOVER_GLOW_WIDTH_MIN}
|
||||
max=${HOVER_GLOW_WIDTH_MAX}
|
||||
step=${HOVER_GLOW_WIDTH_STEP}
|
||||
.value=${String(hoverGlowWidth)}
|
||||
@input=${this.onHoverGlowWidthChange}
|
||||
class="w-full border border-slate-500 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400 w-12 text-right">
|
||||
${hoverGlowWidth.toFixed(1)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("graphics_setting.hover_glow_alpha_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("graphics_setting.hover_glow_alpha_desc")}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min=${HOVER_GLOW_ALPHA_MIN}
|
||||
max=${HOVER_GLOW_ALPHA_MAX}
|
||||
step=${HOVER_GLOW_ALPHA_STEP}
|
||||
.value=${String(hoverGlowAlpha)}
|
||||
@input=${this.onHoverGlowAlphaChange}
|
||||
class="w-full border border-slate-500 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400 w-12 text-right">
|
||||
${hoverGlowAlpha.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click=${this.onToggleNamesColored}
|
||||
|
||||
@@ -8,6 +8,8 @@ export const GraphicsOverridesSchema = z
|
||||
cullThreshold: z.number(),
|
||||
darkNames: z.boolean(),
|
||||
hoverFadeAlpha: z.number(),
|
||||
hoverGlowWidth: z.number(),
|
||||
hoverGlowAlpha: z.number(),
|
||||
})
|
||||
.partial(),
|
||||
structure: z
|
||||
|
||||
@@ -21,6 +21,12 @@ export function applyGraphicsOverrides(
|
||||
if (overrides.name?.hoverFadeAlpha !== undefined) {
|
||||
settings.name.hoverFadeAlpha = overrides.name.hoverFadeAlpha;
|
||||
}
|
||||
if (overrides.name?.hoverGlowWidth !== undefined) {
|
||||
settings.name.hoverGlowWidth = overrides.name.hoverGlowWidth;
|
||||
}
|
||||
if (overrides.name?.hoverGlowAlpha !== undefined) {
|
||||
settings.name.hoverGlowAlpha = overrides.name.hoverGlowAlpha;
|
||||
}
|
||||
if (overrides.structure?.classicIcons === true) {
|
||||
// Classic look: lighter player-colored shape behind a dark icon glyph,
|
||||
// with a touch of translucency.
|
||||
|
||||
@@ -243,6 +243,10 @@ export interface RenderSettings {
|
||||
statusRowOffset: number;
|
||||
/** Alpha multiplier applied to a name while the cursor is over it. */
|
||||
hoverFadeAlpha: number;
|
||||
/** White glow behind the hovered player's name: px past the outline. */
|
||||
hoverGlowWidth: number;
|
||||
/** Peak opacity of the hover glow (0 disables it). */
|
||||
hoverGlowAlpha: number;
|
||||
};
|
||||
fx: {
|
||||
shockwaveRingWidth: number;
|
||||
|
||||
@@ -326,6 +326,8 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] {
|
||||
slider(s.name, "emojiRowOffset", d.name, 0, 5, 0.1, "Emoji Row Offset"),
|
||||
slider(s.name, "statusRowOffset", d.name, 0, 5, 0.1, "Status Row Offset"),
|
||||
slider(s.name, "hoverFadeAlpha", d.name, 0, 1, 0.05, "Hover Fade Alpha"),
|
||||
slider(s.name, "hoverGlowWidth", d.name, 0, 8, 0.25, "Hover Glow Width"),
|
||||
slider(s.name, "hoverGlowAlpha", d.name, 0, 1, 0.05, "Hover Glow Alpha"),
|
||||
]),
|
||||
|
||||
folder("FX", [
|
||||
|
||||
@@ -49,6 +49,8 @@ export class TextProgram {
|
||||
private uOutlineColor: WebGLUniformLocation;
|
||||
private uOutlineUsePlayerColor: WebGLUniformLocation;
|
||||
private uFillUsePlayerColor: WebGLUniformLocation;
|
||||
private uHoverGlowWidth: WebGLUniformLocation;
|
||||
private uHoverGlowAlpha: WebGLUniformLocation;
|
||||
|
||||
private distanceRange: number;
|
||||
|
||||
@@ -128,6 +130,14 @@ export class TextProgram {
|
||||
this.program,
|
||||
"uFillUsePlayerColor",
|
||||
)!;
|
||||
this.uHoverGlowWidth = gl.getUniformLocation(
|
||||
this.program,
|
||||
"uHoverGlowWidth",
|
||||
)!;
|
||||
this.uHoverGlowAlpha = gl.getUniformLocation(
|
||||
this.program,
|
||||
"uHoverGlowAlpha",
|
||||
)!;
|
||||
|
||||
this.loadAtlas();
|
||||
}
|
||||
@@ -188,6 +198,8 @@ export class TextProgram {
|
||||
ns.outlineUsePlayerColor ? 1.0 : 0.0,
|
||||
);
|
||||
gl.uniform1f(this.uFillUsePlayerColor, ns.fillUsePlayerColor ? 1.0 : 0.0);
|
||||
gl.uniform1f(this.uHoverGlowWidth, ns.hoverGlowWidth);
|
||||
gl.uniform1f(this.uHoverGlowAlpha, ns.hoverGlowAlpha);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.atlasTex!);
|
||||
|
||||
@@ -207,7 +207,9 @@
|
||||
"nameShadeBot": 0.4,
|
||||
"emojiRowOffset": 1.4,
|
||||
"statusRowOffset": 1.4,
|
||||
"hoverFadeAlpha": 0.25
|
||||
"hoverFadeAlpha": 0.5,
|
||||
"hoverGlowWidth": 5,
|
||||
"hoverGlowAlpha": 0.75
|
||||
},
|
||||
"fx": {
|
||||
"shockwaveRingWidth": 0.04,
|
||||
|
||||
@@ -8,10 +8,13 @@ uniform float uNightAmbient;
|
||||
uniform vec3 uOutlineColor;
|
||||
uniform float uOutlineUsePlayerColor;
|
||||
uniform float uFillUsePlayerColor;
|
||||
uniform float uHoverGlowWidth; // px the white hover glow extends past the outline
|
||||
uniform float uHoverGlowAlpha; // peak opacity of the hover glow
|
||||
|
||||
in vec2 vUV;
|
||||
in vec4 vPlayerColor; // player territory color (rgb) + alpha
|
||||
in float vNameShade; // name fill grayscale shade (0.0 = black)
|
||||
flat in float vHighlight; // 1.0 when this player is hovered (white glow)
|
||||
out vec4 fragColor;
|
||||
|
||||
float median(float r, float g, float b) {
|
||||
@@ -42,20 +45,40 @@ void main() {
|
||||
float screenPxDist = screenPxRange * (sd - 0.5);
|
||||
float fillAlpha = clamp(screenPxDist + 0.5, 0.0, 1.0);
|
||||
|
||||
if (uOutlineWidth > 0.0) {
|
||||
// The SDF saturates at sd=0 (screenPxDist = -screenPxRange*0.5).
|
||||
// Reserve a 1px margin so saturated fragments always get alpha=0.
|
||||
float maxOutline = max(screenPxRange * 0.5 - 1.0, 0.0);
|
||||
float effectiveOutline = min(uOutlineWidth, maxOutline);
|
||||
// The SDF saturates at sd=0 (screenPxDist = -screenPxRange*0.5).
|
||||
// Reserve a 1px margin so saturated fragments always get alpha=0.
|
||||
float maxOutline = max(screenPxRange * 0.5 - 1.0, 0.0);
|
||||
float effectiveOutline = min(uOutlineWidth, maxOutline);
|
||||
|
||||
vec3 color;
|
||||
float coverage;
|
||||
if (uOutlineWidth > 0.0) {
|
||||
float outlineDist = screenPxDist + effectiveOutline;
|
||||
float outlineAlpha = clamp(outlineDist + 0.5, 0.0, 1.0);
|
||||
|
||||
vec3 nightOutlineColor = mix(vec3(0.0), uOutlineColor, borderT);
|
||||
vec3 borderColor = mix(nightOutlineColor, vPlayerColor.rgb, uOutlineUsePlayerColor);
|
||||
vec3 color = mix(borderColor, fillColor, fillAlpha);
|
||||
fragColor = vec4(color, vPlayerColor.a * outlineAlpha);
|
||||
color = mix(borderColor, fillColor, fillAlpha);
|
||||
coverage = outlineAlpha;
|
||||
} else {
|
||||
fragColor = vec4(fillColor, vPlayerColor.a * fillAlpha);
|
||||
color = fillColor;
|
||||
coverage = fillAlpha;
|
||||
}
|
||||
|
||||
// Soft white glow behind the hovered player's name. Width is clamped to
|
||||
// the SDF margin past the outline so it never hard-clips at the quad edge.
|
||||
if (vHighlight > 0.5 && uHoverGlowAlpha > 0.0) {
|
||||
float glowWidth = min(uHoverGlowWidth, max(maxOutline - effectiveOutline, 0.0));
|
||||
if (glowWidth > 0.0) {
|
||||
float g = clamp(1.0 + (screenPxDist + effectiveOutline) / glowWidth, 0.0, 1.0);
|
||||
float glowAlpha = g * g * uHoverGlowAlpha;
|
||||
float total = coverage + glowAlpha * (1.0 - coverage);
|
||||
if (total > 0.0) {
|
||||
color = mix(vec3(1.0), color, coverage / total);
|
||||
coverage = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragColor = vec4(color, vPlayerColor.a * coverage);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ uniform float uHoverFadeAlpha; // alpha multiplier applied to that player's name
|
||||
out vec2 vUV;
|
||||
out vec4 vPlayerColor; // player territory color (rgb) + alpha
|
||||
out float vNameShade; // name fill grayscale shade (0.0 = black)
|
||||
flat out float vHighlight; // 1.0 when this player is hovered (white glow)
|
||||
|
||||
void main() {
|
||||
// 1. Decode instance ID → playerIdx, lineIdx, charPos
|
||||
@@ -57,6 +58,7 @@ void main() {
|
||||
vUV = vec2(0.0);
|
||||
vPlayerColor = vec4(0.0);
|
||||
vNameShade = 0.0;
|
||||
vHighlight = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,6 +69,7 @@ void main() {
|
||||
vUV = vec2(0.0);
|
||||
vPlayerColor = vec4(0.0);
|
||||
vNameShade = 0.0;
|
||||
vHighlight = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,6 +81,7 @@ void main() {
|
||||
vUV = vec2(0.0);
|
||||
vPlayerColor = vec4(0.0);
|
||||
vNameShade = 0.0;
|
||||
vHighlight = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,6 +116,7 @@ void main() {
|
||||
vUV = vec2(0.0);
|
||||
vPlayerColor = vec4(0.0);
|
||||
vNameShade = 0.0;
|
||||
vHighlight = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -136,6 +141,7 @@ void main() {
|
||||
vUV = vec2(0.0);
|
||||
vPlayerColor = vec4(0.0);
|
||||
vNameShade = 0.0;
|
||||
vHighlight = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,4 +172,5 @@ void main() {
|
||||
vUV = vec2(mix(u0, u1, aPos.x), mix(v0, v1, aPos.y));
|
||||
vPlayerColor = vec4(pd2.rgb, pd2.a * hoverAlpha); // player territory color + alpha
|
||||
vNameShade = pd3.z; // name fill grayscale shade (0.0 = black)
|
||||
vHighlight = isHighlighted ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ describe("GraphicsOverridesSchema", () => {
|
||||
{ name: { nameScaleFactor: 0.8 } },
|
||||
{ name: { cullThreshold: 0.02 } },
|
||||
{ name: { darkNames: true } },
|
||||
{ name: { hoverGlowWidth: 5 } },
|
||||
{ name: { hoverGlowAlpha: 0.6 } },
|
||||
{ name: { nameScaleFactor: 1.2, cullThreshold: 0, darkNames: false } },
|
||||
{ name: { hoverGlowWidth: 0, hoverGlowAlpha: 0 } },
|
||||
];
|
||||
for (const c of cases) {
|
||||
expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true);
|
||||
@@ -74,6 +77,14 @@ describe("GraphicsOverridesSchema", () => {
|
||||
expect(
|
||||
GraphicsOverridesSchema.safeParse({ name: { darkNames: "yes" } }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
GraphicsOverridesSchema.safeParse({ name: { hoverGlowWidth: "wide" } })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
GraphicsOverridesSchema.safeParse({ name: { hoverGlowAlpha: true } })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
GraphicsOverridesSchema.safeParse({
|
||||
structure: { classicIcons: "yes" },
|
||||
@@ -132,6 +143,28 @@ describe("applyGraphicsOverrides", () => {
|
||||
expect(gen({ name: { cullThreshold: 0 } }).name.cullThreshold).toBe(0);
|
||||
});
|
||||
|
||||
test("applies hoverGlowWidth override (including 0)", () => {
|
||||
expect(gen({ name: { hoverGlowWidth: 6 } }).name.hoverGlowWidth).toBe(6);
|
||||
expect(gen({ name: { hoverGlowWidth: 0 } }).name.hoverGlowWidth).toBe(0);
|
||||
});
|
||||
|
||||
test("applies hoverGlowAlpha override (including 0)", () => {
|
||||
expect(gen({ name: { hoverGlowAlpha: 0.9 } }).name.hoverGlowAlpha).toBe(
|
||||
0.9,
|
||||
);
|
||||
expect(gen({ name: { hoverGlowAlpha: 0 } }).name.hoverGlowAlpha).toBe(0);
|
||||
});
|
||||
|
||||
test("hover glow overrides leave other name fields at defaults", () => {
|
||||
const defaults = createRenderSettings().name;
|
||||
const s = gen({
|
||||
name: { hoverGlowWidth: 7, hoverGlowAlpha: 0.1 },
|
||||
}).name;
|
||||
expect(s.hoverFadeAlpha).toBe(defaults.hoverFadeAlpha);
|
||||
expect(s.nameScaleFactor).toBe(defaults.nameScaleFactor);
|
||||
expect(s.cullThreshold).toBe(defaults.cullThreshold);
|
||||
});
|
||||
|
||||
test("darkNames=true → black fill + player-colored outline + outline RGB 0", () => {
|
||||
const s = gen({ name: { darkNames: true } }).name;
|
||||
expect(s.fillUsePlayerColor).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user