Worker orbs: cache glow sprites for perf + SoulSync logo in enrichment modal topbar
Performance: - Bake one soft glow sprite per colour into an offscreen canvas and blit it with drawImage instead of allocating a radial gradient every frame. This was the hot path: sparks + inbound pulses + every orb glow each built a gradient per frame (100+/frame at 60fps). Colours quantised to 8-step buckets to bound the cache (imperceptible tint shift, keeps the rainbow path from minting a sprite every frame). - Cache each orb's button element at init so the 30-frame active-state check no longer re-runs querySelector. - Net: the pulses/glows look identical, far fewer allocations per frame. UI: - Enrichment manager modal topbar icon now uses the SoulSync logo (trans2.png) instead of the helix emoji, matching the dashboard button.
This commit is contained in:
parent
6dfe9f3d6a
commit
634b9ca0a0
3 changed files with 49 additions and 34 deletions
|
|
@ -149,7 +149,7 @@ async function openEnrichmentManager(workerId) {
|
||||||
<div class="enrichment-manager-modal" role="dialog" aria-modal="true"
|
<div class="enrichment-manager-modal" role="dialog" aria-modal="true"
|
||||||
aria-label="Manage Enrichment Workers" tabindex="-1">
|
aria-label="Manage Enrichment Workers" tabindex="-1">
|
||||||
<div class="em-topbar">
|
<div class="em-topbar">
|
||||||
<div class="em-topbar-icon"><span>🧬</span></div>
|
<div class="em-topbar-icon"><img src="/static/trans2.png" alt="SoulSync" class="em-topbar-logo"></div>
|
||||||
<div class="em-topbar-titles">
|
<div class="em-topbar-titles">
|
||||||
<h3 class="em-topbar-title">Enrichment Workers</h3>
|
<h3 class="em-topbar-title">Enrichment Workers</h3>
|
||||||
<div class="em-topbar-sub">Match your library across every metadata source</div>
|
<div class="em-topbar-sub">Match your library across every metadata source</div>
|
||||||
|
|
|
||||||
|
|
@ -64948,6 +64948,7 @@ body.reduce-effects .dash-card::after {
|
||||||
box-shadow: 0 6px 20px rgba(var(--accent-rgb), 0.45), inset 0 1px 0 rgba(255,255,255,0.25);
|
box-shadow: 0 6px 20px rgba(var(--accent-rgb), 0.45), inset 0 1px 0 rgba(255,255,255,0.25);
|
||||||
}
|
}
|
||||||
.em-topbar-icon span { filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); }
|
.em-topbar-icon span { filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); }
|
||||||
|
.em-topbar-logo { width: 32px; height: 32px; object-fit: contain; display: block; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); }
|
||||||
.em-topbar-titles { flex: 1 1 auto; min-width: 0; }
|
.em-topbar-titles { flex: 1 1 auto; min-width: 0; }
|
||||||
.em-topbar-title {
|
.em-topbar-title {
|
||||||
margin: 0; font-size: 19px; font-weight: 800; letter-spacing: 0.2px;
|
margin: 0; font-size: 19px; font-weight: 800; letter-spacing: 0.2px;
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@
|
||||||
|
|
||||||
orbs.push({
|
orbs.push({
|
||||||
el,
|
el,
|
||||||
|
btn: el.matches('button') ? el : el.querySelector('button'),
|
||||||
color: def.color,
|
color: def.color,
|
||||||
rainbow: def.rainbow || false,
|
rainbow: def.rainbow || false,
|
||||||
hub: def.hub || false,
|
hub: def.hub || false,
|
||||||
|
|
@ -161,6 +162,44 @@
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Glow sprite cache ──
|
||||||
|
// Radial gradients are the expensive part of canvas glows. Bake one soft
|
||||||
|
// glow sprite per colour into an offscreen canvas and blit it with
|
||||||
|
// drawImage — a single cheap GPU copy instead of allocating a gradient
|
||||||
|
// every frame. Colours are quantised to 8-step buckets to bound the cache
|
||||||
|
// (the tint shift is imperceptible in a glow, and keeps the rainbow path
|
||||||
|
// from minting a new sprite every frame).
|
||||||
|
const GLOW_SIZE = 64;
|
||||||
|
const _glowCache = new Map();
|
||||||
|
|
||||||
|
function getGlowSprite(r, g, b) {
|
||||||
|
const qr = r & ~7, qg = g & ~7, qb = b & ~7;
|
||||||
|
const key = (qr << 16) | (qg << 8) | qb;
|
||||||
|
let spr = _glowCache.get(key);
|
||||||
|
if (spr) return spr;
|
||||||
|
|
||||||
|
spr = document.createElement('canvas');
|
||||||
|
spr.width = spr.height = GLOW_SIZE;
|
||||||
|
const gctx = spr.getContext('2d');
|
||||||
|
const c = GLOW_SIZE / 2;
|
||||||
|
const grad = gctx.createRadialGradient(c, c, 0, c, c, c);
|
||||||
|
grad.addColorStop(0, `rgba(${qr}, ${qg}, ${qb}, 1)`);
|
||||||
|
grad.addColorStop(1, `rgba(${qr}, ${qg}, ${qb}, 0)`);
|
||||||
|
gctx.fillStyle = grad;
|
||||||
|
gctx.fillRect(0, 0, GLOW_SIZE, GLOW_SIZE);
|
||||||
|
|
||||||
|
_glowCache.set(key, spr);
|
||||||
|
return spr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blit a cached glow of the given radius/alpha centred at (x, y)
|
||||||
|
function drawGlow(ctx, x, y, radius, r, g, b, alpha) {
|
||||||
|
if (alpha <= 0 || radius <= 0) return;
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.drawImage(getGlowSprite(r, g, b), x - radius, y - radius, radius * 2, radius * 2);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Spark system ──
|
// ── Spark system ──
|
||||||
|
|
||||||
function emitSpark(orb, colorOverride) {
|
function emitSpark(orb, colorOverride) {
|
||||||
|
|
@ -199,14 +238,8 @@
|
||||||
const alpha = s.life * 0.6;
|
const alpha = s.life * 0.6;
|
||||||
const radius = s.radius * s.life;
|
const radius = s.radius * s.life;
|
||||||
|
|
||||||
// Spark glow
|
// Spark glow (cached sprite)
|
||||||
const glow = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, radius * 3);
|
drawGlow(ctx, s.x, s.y, radius * 3, r, g, b, alpha * 0.4);
|
||||||
glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha * 0.4})`);
|
|
||||||
glow.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(s.x, s.y, radius * 3, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = glow;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// Spark core
|
// Spark core
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
@ -248,13 +281,7 @@
|
||||||
const alpha = 0.55 * (1 - Math.abs(p.t - 0.5) * 0.6); // fade in/out at the ends
|
const alpha = 0.55 * (1 - Math.abs(p.t - 0.5) * 0.6); // fade in/out at the ends
|
||||||
const radius = 2.2;
|
const radius = 2.2;
|
||||||
|
|
||||||
const glow = ctx.createRadialGradient(x, y, 0, x, y, radius * 3);
|
drawGlow(ctx, x, y, radius * 3, r, g, b, alpha * 0.5);
|
||||||
glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha * 0.5})`);
|
|
||||||
glow.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(x, y, radius * 3, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = glow;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||||
|
|
@ -420,12 +447,11 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check active state every 30 frames
|
// Check active state every 30 frames (button ref is cached at init)
|
||||||
if (frameCount % 30 === 0) {
|
if (frameCount % 30 === 0) {
|
||||||
orbs.forEach(orb => {
|
orbs.forEach(orb => {
|
||||||
orb.visible = orb.el.offsetParent !== null;
|
orb.visible = orb.el.offsetParent !== null;
|
||||||
const btn = orb.el.querySelector('button');
|
orb.active = orb.btn ? orb.btn.classList.contains('active') : false;
|
||||||
orb.active = btn ? btn.classList.contains('active') : false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -580,15 +606,9 @@
|
||||||
const slow = 0.5 + 0.5 * Math.sin(time * beatSpeed);
|
const slow = 0.5 + 0.5 * Math.sin(time * beatSpeed);
|
||||||
const hubR = (ORB_RADIUS + 3 + energy * 4) + slow * (2 + energy * 2);
|
const hubR = (ORB_RADIUS + 3 + energy * 4) + slow * (2 + energy * 2);
|
||||||
|
|
||||||
// Wide ambient glow — brighter + wider with energy
|
// Wide ambient glow — brighter + wider with energy (cached sprite)
|
||||||
const glowR = hubR * (4 + energy * 1.5);
|
const glowR = hubR * (4 + energy * 1.5);
|
||||||
const halo = ctx.createRadialGradient(orb.x, orb.y, 0, orb.x, orb.y, glowR);
|
drawGlow(ctx, orb.x, orb.y, glowR, r, g, b, 0.18 + energy * 0.18 + slow * 0.12);
|
||||||
halo.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${0.18 + energy * 0.18 + slow * 0.12})`);
|
|
||||||
halo.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(orb.x, orb.y, glowR, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = halo;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
if (hubImageReady) {
|
if (hubImageReady) {
|
||||||
// SoulSync logo as the nucleus — fit to the pulsing radius while
|
// SoulSync logo as the nucleus — fit to the pulsing radius while
|
||||||
|
|
@ -650,13 +670,7 @@
|
||||||
const glowAlpha = orb.active
|
const glowAlpha = orb.active
|
||||||
? (0.25 + pulse * 0.2) * activeMult
|
? (0.25 + pulse * 0.2) * activeMult
|
||||||
: (0.06 + pulse * 0.03) * activeMult;
|
: (0.06 + pulse * 0.03) * activeMult;
|
||||||
const glow = ctx.createRadialGradient(orb.x, orb.y, 0, orb.x, orb.y, glowRadius);
|
drawGlow(ctx, orb.x, orb.y, glowRadius, r, g, b, glowAlpha);
|
||||||
glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${glowAlpha})`);
|
|
||||||
glow.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(orb.x, orb.y, glowRadius, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = glow;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
const coreAlpha = orb.active
|
const coreAlpha = orb.active
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue