fix: hero fades in complete — no more incremental load flicker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-07 22:19:37 +02:00
1 parent e2964829bd
commit 42fe5d802b
1 file changed
+21 -11
+21 -11
View File
@@ -69,6 +69,8 @@ export async function loadArtistDetail(artist) {
const simEl = document.getElementById('artist-similar-section'); const simEl = document.getElementById('artist-similar-section');
const tsEl = document.getElementById('artist-topsongs-section'); const tsEl = document.getElementById('artist-topsongs-section');
const albEl = document.getElementById('artist-albums-section'); const albEl = document.getElementById('artist-albums-section');
hero.style.opacity = '0';
hero.style.transition = '';
hero.style.background = 'linear-gradient(135deg, var(--surface2), var(--surface))'; hero.style.background = 'linear-gradient(135deg, var(--surface2), var(--surface))';
hero.querySelector('.artist-hero-img')?.remove(); hero.querySelector('.artist-hero-img')?.remove();
hero.querySelector('.artist-hero-tint')?.remove(); hero.querySelector('.artist-hero-tint')?.remove();
@@ -111,12 +113,18 @@ export async function loadArtistDetail(artist) {
]) ])
: [{ status: 'rejected' }, { status: 'rejected' }]; : [{ status: 'rejected' }, { status: 'rejected' }];
// ── Hero ── // ── Hero ── (await color before showing anything so it all renders at once)
const img = imgRes.status === 'fulfilled' ? imgRes.value : null; const img = imgRes.status === 'fulfilled' ? imgRes.value : null;
const _firstAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : []; const _firstAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : [];
const _bgCover = _firstAlbums[0] ? coverUrl(_firstAlbums[0].coverArt, 512) : null; const _bgCover = _firstAlbums[0] ? coverUrl(_firstAlbums[0].coverArt, 512) : null;
// Option 1: artist photo as background (falls back to album art)
const bgSrc = img || _bgCover; const bgSrc = img || _bgCover;
// _extractColor loads img into canvas — warms the browser cache for the display uses below
const [rgb] = await Promise.all([
img ? _extractColor(img) : Promise.resolve(null),
bgSrc && bgSrc !== img ? new Promise(r => { const i = new Image(); i.onload = i.onerror = r; i.src = bgSrc; }) : Promise.resolve(),
]);
if(bgSrc) { if(bgSrc) {
hero.style.background = ''; hero.style.background = '';
const bgEl = document.getElementById('artist-hero-bg'); const bgEl = document.getElementById('artist-hero-bg');
@@ -133,16 +141,18 @@ export async function loadArtistDetail(artist) {
const initial = document.getElementById('artist-hero-pfp-initial'); const initial = document.getElementById('artist-hero-pfp-initial');
if(initial) initial.style.display = 'none'; if(initial) initial.style.display = 'none';
} }
// Option 2: dominant color tint — async, non-blocking
_extractColor(img).then(rgb => {
if(!rgb) return;
const [r, g, b] = rgb;
const tint = document.createElement('div');
tint.className = 'artist-hero-tint';
tint.style.cssText = `position:absolute;inset:0;background:linear-gradient(to right,rgba(${r},${g},${b},.55) 0%,rgba(${r},${g},${b},.25) 55%,rgba(${r},${g},${b},.6) 100%);pointer-events:none`;
if(overlay) hero.insertBefore(tint, overlay);
});
} }
if(rgb) {
const [r, g, b] = rgb;
const tint = document.createElement('div');
tint.className = 'artist-hero-tint';
tint.style.cssText = `position:absolute;inset:0;background:linear-gradient(to right,rgba(${r},${g},${b},.55) 0%,rgba(${r},${g},${b},.25) 55%,rgba(${r},${g},${b},.6) 100%);pointer-events:none`;
if(overlay) hero.insertBefore(tint, overlay);
}
requestAnimationFrame(() => {
hero.style.transition = 'opacity 0.3s ease';
hero.style.opacity = '1';
});
// ── Bio ── // ── Bio ──
const info = infoRes.status === 'fulfilled' ? infoRes.value?.artistInfo2 : null; const info = infoRes.status === 'fulfilled' ? infoRes.value?.artistInfo2 : null;