diff --git a/webui/static/discover.js b/webui/static/discover.js
index b4b1e4d8..c3b0300a 100644
--- a/webui/static/discover.js
+++ b/webui/static/discover.js
@@ -5533,8 +5533,9 @@ function _artMapDrawNodeToBuffer(octx, n, scale) {
octx.fill();
}
- // Glassy specular highlight (orb look) — skip the tiniest where it'd be noise.
- if (rScaled >= 5) {
+ // Glassy specular highlight (orb look) — only on bubbles big enough to read
+ // it; skipping the dense swarm halves per-frame drawImage cost when zoomed in.
+ if (rScaled >= 12) {
octx.drawImage(_artMapGlossSprite(), n.x - r, n.y - r, r * 2, r * 2);
}
@@ -5694,10 +5695,14 @@ function _artMapStartLoop() {
if (!a.running) return;
_artMap._now = t;
const more = _artMapStepAnimations(t);
- _artMapDraw(); // sets _artMap._liveCount
- // Keep looping while something is animating, OR while ambient buoyancy is
- // on AND there are live bubbles on screen to bob (so a zoomed-out static
- // overview costs nothing — the loop parks until the next interaction).
+ // Reveal/ripple → full 60fps. Idle buoyancy → ~30fps (the gentle bob
+ // doesn't need 60, and halving the redraws keeps dense zoomed-in maps
+ // smooth + cool).
+ const ambientOnly = !more && _artMap._ambient;
+ if (!ambientOnly || (t - (a._lastDraw || 0)) >= 32) {
+ _artMapDraw(); // sets _artMap._liveCount
+ a._lastDraw = t;
+ }
const keep = more || (_artMap._ambient && _artMap._liveCount > 0 && !document.hidden);
if (keep) {
a.raf = requestAnimationFrame(tick);
@@ -5996,23 +6001,21 @@ function _artMapDraw() {
ctx.stroke();
}
- // Circle + image
- ctx.save();
- ctx.beginPath();
- ctx.arc(hn.x, hn.y, r, 0, Math.PI * 2);
- ctx.closePath();
- ctx.clip();
-
+ // Circle + image — pre-masked, so no per-frame clip (that was
+ // the hover-lag culprit once the ambient loop forced redraws).
const img = _artMap.images[hn.id];
if (img) {
ctx.drawImage(img, hn.x - r, hn.y - r, r * 2, r * 2);
- ctx.fillStyle = 'rgba(0,0,0,0.35)';
- ctx.fillRect(hn.x - r, hn.y - r, r * 2, r * 2);
+ ctx.beginPath();
+ ctx.arc(hn.x, hn.y, r, 0, Math.PI * 2);
+ ctx.fillStyle = 'rgba(0,0,0,0.35)'; // keep the name legible over art
+ ctx.fill();
} else {
+ ctx.beginPath();
+ ctx.arc(hn.x, hn.y, r, 0, Math.PI * 2);
ctx.fillStyle = isW ? '#1a0a30' : '#141420';
- ctx.fillRect(hn.x - r, hn.y - r, r * 2, r * 2);
+ ctx.fill();
}
- ctx.restore();
// Border
ctx.beginPath();
@@ -6155,7 +6158,13 @@ function _artMapShowTooltip(e, node) {
// the hovered artist actually changes — a plain mousemove just repositions.
if (_artMap._tipNodeId !== node.id) {
_artMap._tipNodeId = node.id;
- const img = node.image_url ? `` : '