Hover polish: constant-radius glow (opacity fade only), instant text swap

- The halo no longer grows during the fade: radius is constant at
  size + 9px; only its opacity fades in/out over ~180ms.
- The hover name is drawn opaque in the bright color instead of
  cross-fading over the muted resting label. Partial-alpha text
  oscillated whenever hover toggles rapidly at a small node's hit
  boundary (sigma fires enterNode/leaveNode on every state change) —
  an un-animated color can't flicker.
This commit is contained in:
amosblomqvist
2026-09-02 02:31:08 +03:00
parent 0a8c360546
commit 96da582d35
+21 -19
View File
@@ -15,12 +15,14 @@
* - defaultDrawNodeLabel: plain wrapped text, centered below the node,
* word-wrapped at a max line width — no truncation, no one-line sprawl.
* - defaultDrawNodeHover: NO background box (sigma's default is a
* hardcoded white one). Instead the hovered node gets a STATIC soft
* glow halo in its own state color, flush with the disc edge, and
* its name brightens — the "this is clickable, click for the full
* fact" affordance. Entering/leaving hover cross-fades (glow and text
* together) over a short transition; the glow itself never pulses.
* Hover always shows the name, even when labels are zoomed out.
* hardcoded white one). Instead the hovered node gets a soft glow
* halo in its own state color at a constant radius, flush with the
* disc edge, and its name brightens instantly — the "this is
* clickable, click for the full fact" affordance. Entering/leaving
* hover fades the halo's OPACITY over a short transition (no
* growth, no pulse; the text color is un-animated so rapid hover
* toggles at a node's edge can't flicker it). Hover always shows
* the name, even when labels are zoomed out.
* Labels are zoom-gated GLOBALLY with a fade: all names appear at once
* when the camera is at fit zoom or closer (ratio <= 1), then fade out
* together across a short zoom span before label rendering switches off —
@@ -73,13 +75,13 @@ const LABELS_CAMERA_RATIO = 1;
/** Zoom span (camera-ratio units) over which labels fade out together. */
const LABELS_FADE_SPAN = 0.5;
// Hover glow: a STATIC soft halo ring in the node's own color, starting
// flush at the disc edge (no gap — a gap reads as a false outline/border
// between node and glow). The hovered/unhovered transition cross-fades
// glow + text over a short window; the glow never pulses.
const GLOW_PAD = 9; // px the halo extends beyond the node disc
// Hover glow: a soft halo ring in the node's own color at a CONSTANT
// radius, starting flush at the disc edge (no gap — a gap reads as a
// false outline/border between node and glow). Only the halo's OPACITY
// animates (hover enter/leave); it never grows and never pulses.
const GLOW_PAD = 9; // px the halo extends beyond the node disc (constant)
const GLOW_ALPHA = 0.45; // halo opacity at the disc edge, fading outward
const HOVER_FADE_MS = 180; // hover enter/leave transition length
const HOVER_FADE_MS = 180; // hover halo opacity fade length
/** "#rrggbb" + alpha → "rgba(...)" (tokenHex only ever emits #rrggbb). */
function rgbaFromHex(hex: string, alpha: number): string {
@@ -140,11 +142,12 @@ function drawName(
}
/** The hovered-state paint at a given transition factor (0 = resting,
* 1 = fully hovered): a static glow halo in the node's own state color
* starting flush at the disc edge (the evenodd punch-out keeps the disc
* itself unpainted), plus the name at partial alpha — the muted label on
* the layer beneath shows through, so the text cross-fades toward the
* brighter hover color as the glow blooms. */
* 1 = fully hovered): a glow halo in the node's own state color at a
* CONSTANT radius (opacity fades in/out; the halo never grows) starting
* flush at the disc edge (the evenodd punch-out keeps the disc itself
* unpainted). The name is drawn opaque in the bright hover color — no
* alpha animation: partial-alpha text over the resting label flickers
* when hover toggles rapidly at a node's edge. */
function paintHover(
context: CanvasRenderingContext2D,
data: { label?: string | null; x: number; y: number; size: number; color: string },
@@ -155,7 +158,7 @@ function paintHover(
context.save();
if (fade > 0.02) {
const inner = data.size;
const outer = data.size + GLOW_PAD * fade;
const outer = data.size + GLOW_PAD;
const glow = context.createRadialGradient(data.x, data.y, inner, data.x, data.y, outer);
glow.addColorStop(0, rgbaFromHex(data.color, GLOW_ALPHA * fade));
glow.addColorStop(1, rgbaFromHex(data.color, 0));
@@ -165,7 +168,6 @@ function paintHover(
context.arc(data.x, data.y, inner, 0, Math.PI * 2, true);
context.fill("evenodd");
}
context.globalAlpha = fade;
drawName(context, data, settings, hoverTextColor);
context.restore();
}