← back to Glitterwalls
hero: static hero-bg.jpg back as position 1, then glitter carousel (headers cropped)
733a126a4186bac31d78a35c9bfa9d8b8732c774 · 2026-07-30 11:46:49 -0700 · Steve Abrams
- hero-bg.jpg painted immediately on load = position 1, before /api/products resolves
- glitter product images crossfade after it and loop back to the static image
- crossfade overlay now gets the center-12% crop (it previously bypassed the
anti-header rule), so vendor banner/header chrome is lopped off every frame
- retired the <4-hi-res 2x2 grid fallback; static-only when no product images
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 733a126a4186bac31d78a35c9bfa9d8b8732c774
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 30 11:46:49 2026 -0700
hero: static hero-bg.jpg back as position 1, then glitter carousel (headers cropped)
- hero-bg.jpg painted immediately on load = position 1, before /api/products resolves
- glitter product images crossfade after it and loop back to the static image
- crossfade overlay now gets the center-12% crop (it previously bypassed the
anti-header rule), so vendor banner/header chrome is lopped off every frame
- retired the <4-hi-res 2x2 grid fallback; static-only when no product images
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/index.html | 97 +++++++++++++++++++++----------------------------------
1 file changed, 36 insertions(+), 61 deletions(-)
diff --git a/public/index.html b/public/index.html
index e5b7049..555e78b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1237,18 +1237,18 @@ loadGridPage();
});
}
}, true);
-// HERO: only true high-res Shopify images (>=1600px wide). If <4 hi-res candidates,
-// fall back to a 2x2 grid of the best-resolution images we have. Locked 2026-05-08.
-(async function heroHiResOrGrid() {
+// HERO: static hero-bg.jpg is POSITION 1 (painted immediately on load), then the
+// glitter product images crossfade after it and loop back to it. The scraped
+// product images carry vendor banner/header chrome, so every crossfade frame is
+// cropped center 12% to lop the headers off. (Static-first-then-carousel — Steve 2026-07-30.)
+(async function heroStaticFirstCarousel() {
const bg = document.getElementById('heroBg');
if (!bg) return;
+ const STATIC_HERO = '/hero-bg.jpg';
const HIRES_MIN = 1600;
+ // Position 1 — clean static image up first, before /api/products resolves.
+ bg.style.backgroundImage = "url('" + STATIC_HERO + "')";
function strip(u){ return (u||'').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4'); }
- function bumpWidth(u, w){
- if (!u) return u;
- const sized = u.replace(/(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '_'+w+'x$1$3');
- return sized;
- }
function probe(url) {
return new Promise((resolve) => {
const im = new Image();
@@ -1257,6 +1257,7 @@ loadGridPage();
im.src = url;
});
}
+ function shuffle(a){ for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];} return a; }
try {
const r = await fetch('/api/products?limit=200');
if (!r.ok) return;
@@ -1265,64 +1266,38 @@ loadGridPage();
.filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
.map(p => ({ title: p.title, url: strip(p.image_url) }))
.slice(0, 24);
- if (cands.length === 0) return;
+ if (cands.length === 0) return; // no product images → static-only hero
// Probe original (un-resized) URLs in parallel — Shopify returns the master file.
const probed = (await Promise.all(cands.map(c => probe(c.url)))).filter(Boolean);
- // Hi-res pool: only images that are genuinely high-end, high-res.
- const hiresPool = probed.filter(p => p.w >= HIRES_MIN);
- // Fisher-Yates shuffle so every page load picks a different set/order.
- function shuffle(a){ for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];} return a; }
- shuffle(hiresPool);
-
- if (hiresPool.length >= 4) {
- // Single-image full-bleed crossfade carousel of hi-res images only,
- // randomized each visit.
- const cycle = hiresPool.slice(0, Math.min(8, hiresPool.length));
- const overlay = document.createElement('div');
- overlay.style.cssText = 'position:absolute;inset:0;background-size:cover;background-position:center;opacity:0;transition:opacity 1.8s ease;z-index:1';
- bg.style.position = 'absolute'; bg.style.inset = '0';
- bg.style.transition = 'opacity 1.8s ease'; bg.style.zIndex = '0';
- bg.parentNode.insertBefore(overlay, bg.nextSibling);
- let idx = 0;
- bg.style.backgroundImage = "url('" + cycle[idx].url + "')";
- setInterval(() => {
- idx = (idx + 1) % cycle.length;
- const nextUrl = cycle[idx].url;
- overlay.style.backgroundImage = "url('" + nextUrl + "')";
- overlay.style.opacity = '1';
- setTimeout(() => {
- bg.style.backgroundImage = "url('" + nextUrl + "')";
- overlay.style.transition = 'none';
- overlay.style.opacity = '0';
- setTimeout(() => { overlay.style.transition = 'opacity 1.8s ease'; }, 50);
- }, 1900);
- }, 6000);
- return;
- }
-
- // 2×2 grid mode — pick 4 fresh tiles each visit. Prefer hi-res, only
- // fall back to the next-best resolution if hi-res pool can't fill 4.
+ // Prefer genuinely hi-res; fall back to next-best so the carousel still runs.
const ranked = probed.slice().sort((a,b) => b.w - a.w);
- const tier1 = ranked.filter(p => p.w >= HIRES_MIN);
- const tier2 = ranked.filter(p => p.w >= 1000 && p.w < HIRES_MIN);
- let pool = tier1.length >= 4 ? tier1 : tier1.concat(tier2);
- shuffle(pool);
- const tiles = pool.slice(0, 4);
- if (tiles.length < 1) return;
- while (tiles.length < 4) tiles.push(tiles[tiles.length % Math.max(1, tiles.length)]);
- bg.style.backgroundImage = 'none';
- bg.style.background = '#000';
+ let productPool = ranked.filter(p => p.w >= HIRES_MIN);
+ if (productPool.length < 4) productPool = productPool.concat(ranked.filter(p => p.w >= 1000 && p.w < HIRES_MIN));
+ shuffle(productPool);
+ if (productPool.length === 0) return; // nothing usable → static-only hero
+
+ // cycle[0] = static hero (position 1); the rest are glitter product images.
+ const cycle = [{ url: STATIC_HERO }].concat(productPool.slice(0, 7));
+ const overlay = document.createElement('div');
+ // center 12% crop lops any banner/header chrome off the incoming crossfade frame.
+ overlay.style.cssText = 'position:absolute;inset:0;background-size:cover;background-position:center 12%;opacity:0;transition:opacity 1.8s ease;z-index:1';
bg.style.position = 'absolute'; bg.style.inset = '0';
- bg.style.display = 'grid';
- bg.style.gridTemplateColumns = '1fr 1fr';
- bg.style.gridTemplateRows = '1fr 1fr';
- bg.style.gap = '2px';
- for (const tile of tiles) {
- const cell = document.createElement('div');
- cell.style.cssText = "background-size:cover;background-position:center;background-image:url('"+tile.url+"');";
- bg.appendChild(cell);
- }
+ bg.style.transition = 'opacity 1.8s ease'; bg.style.zIndex = '0';
+ bg.parentNode.insertBefore(overlay, bg.nextSibling);
+ let idx = 0; // already showing cycle[0] = static hero
+ setInterval(() => {
+ idx = (idx + 1) % cycle.length;
+ const nextUrl = cycle[idx].url;
+ overlay.style.backgroundImage = "url('" + nextUrl + "')";
+ overlay.style.opacity = '1';
+ setTimeout(() => {
+ bg.style.backgroundImage = "url('" + nextUrl + "')";
+ overlay.style.transition = 'none';
+ overlay.style.opacity = '0';
+ setTimeout(() => { overlay.style.transition = 'opacity 1.8s ease'; }, 50);
+ }, 1900);
+ }, 6000);
} catch(e) {}
})();
← 86cd45d search: scroll results into view on mobile (grid was below t
·
back to Glitterwalls
·
sync repo to live: preserve prod hero-bg.jpg + products.json 405d229 →