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:
evanpelle
2026-06-11 15:27:56 -07:00
parent 2d747d0f8b
commit 03a5d691ee
11 changed files with 188 additions and 9 deletions
@@ -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
+6
View File
@@ -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.
+4
View File
@@ -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;
+2
View File
@@ -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!);
+3 -1
View File
@@ -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;
}