fix: revert lazy scene-init - eager pin creation reserves pin-spacing so scenes no longer float over the next section; drop decode-gate that stalled with loading=lazy (kept intrinsic dims, will-change lifecycle, textContent legends)
This commit is contained in:
+38
-42
@@ -921,17 +921,17 @@ body:not(.fx) .slab{display:none}
|
||||
units.push({ sec, man, scene, flat, legend });
|
||||
});
|
||||
|
||||
// Reduced motion: leave the flat exploded hero in place (it IS the end state), no slabs, no scrub.
|
||||
// Reduced motion / no GSAP: leave the flat exploded hero in place (it IS the end state).
|
||||
if (RM || !window.gsap || !window.ScrollTrigger) return;
|
||||
document.body.classList.add("fx");
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const wc = (u, on) => u.slabs && u.slabs.forEach(el => el.style.willChange = on ? "transform" : "auto");
|
||||
const wc = (u, on) => u.slabs.forEach(el => el.style.willChange = on ? "transform" : "auto");
|
||||
|
||||
// Build a scene's slabs lazily, just before it approaches the viewport. The flat image stays the
|
||||
// fallback until the slabs are appended, so a scene that never initialises still reads correctly.
|
||||
const initScene = u => {
|
||||
if (u.slabs) return;
|
||||
// Build every scene's slabs up front so pinned ScrollTriggers reserve their pin-spacing in the
|
||||
// document flow from the start. Creating pins lazily reflows the page and floats a scene over the
|
||||
// following section. The flat hero stays the fallback until its slabs exist.
|
||||
units.forEach(u => {
|
||||
u.slabs = u.man.parts.map(p => {
|
||||
const d = document.createElement("div");
|
||||
d.className = "slab";
|
||||
@@ -941,6 +941,7 @@ body:not(.fx) .slab{display:none}
|
||||
u.scene.appendChild(d);
|
||||
return d;
|
||||
});
|
||||
u.flat.style.visibility = "hidden";
|
||||
const lis = u.legend ? [...u.legend.children] : [];
|
||||
const n = lis.length;
|
||||
u.apply = t => {
|
||||
@@ -954,42 +955,37 @@ body:not(.fx) .slab{display:none}
|
||||
u.scene.style.transform = "scale(" + (0.95 + 0.05 * t).toFixed(4) + ")";
|
||||
if (n) lis.forEach((li, i) => li.classList.toggle("lit", t >= 0.9 * ((i + 0.5) / n)));
|
||||
};
|
||||
u.flat.style.visibility = "hidden";
|
||||
|
||||
const desktop = document.documentElement.scrollHeight - innerHeight > 200 &&
|
||||
matchMedia("(min-width: 901px)").matches;
|
||||
if (desktop) {
|
||||
u.mode = "pin+scrub";
|
||||
ScrollTrigger.create({
|
||||
trigger: u.sec.querySelector(".pin"),
|
||||
start: "top top", end: "+=1800", pin: true, scrub: 0.6,
|
||||
onToggle: self => wc(u, self.isActive),
|
||||
onUpdate: self => u.apply(self.progress),
|
||||
onRefreshInit: () => u.apply(0)
|
||||
});
|
||||
u.apply(0);
|
||||
} else {
|
||||
u.mode = "assemble";
|
||||
u.sec.querySelector(".pin").style.minHeight = "0";
|
||||
u.apply(0); wc(u, true);
|
||||
gsap.to({ v: 0 }, { v: 1, duration: 1.6, ease: "power3.out",
|
||||
onUpdate: function () { u.apply(this.targets()[0].v); },
|
||||
onComplete: () => wc(u, false) });
|
||||
}
|
||||
ScrollTrigger.refresh();
|
||||
};
|
||||
});
|
||||
|
||||
const boot = () => {
|
||||
// initialise each scene shortly before it reaches the viewport; keep the flat image until then
|
||||
const near = new IntersectionObserver((es, obs) => {
|
||||
es.forEach(e => {
|
||||
if (!e.isIntersecting) return;
|
||||
const u = units.find(x => x.sec === e.target);
|
||||
if (u) initScene(u);
|
||||
obs.unobserve(e.target);
|
||||
});
|
||||
}, { rootMargin: "400px 0px 400px 0px" });
|
||||
units.forEach(u => near.observe(u.sec));
|
||||
const desktop = document.documentElement.scrollHeight - innerHeight > 200 &&
|
||||
matchMedia("(min-width: 901px)").matches;
|
||||
units.forEach(u => {
|
||||
if (desktop) {
|
||||
u.mode = "pin+scrub";
|
||||
ScrollTrigger.create({
|
||||
trigger: u.sec.querySelector(".pin"),
|
||||
start: "top top", end: "+=1800", pin: true, scrub: 0.6,
|
||||
onToggle: self => wc(u, self.isActive),
|
||||
onUpdate: self => u.apply(self.progress),
|
||||
onRefreshInit: () => u.apply(0)
|
||||
});
|
||||
u.apply(0);
|
||||
} else {
|
||||
u.mode = "assemble";
|
||||
u.sec.querySelector(".pin").style.minHeight = "0";
|
||||
u.apply(0);
|
||||
// assemble once, when the scene scrolls in; keep will-change only for the tween's life
|
||||
ScrollTrigger.create({
|
||||
trigger: u.scene, start: "top 80%", once: true,
|
||||
onEnter: () => { wc(u, true); gsap.to({ v: 0 }, {
|
||||
v: 1, duration: 1.6, ease: "power3.out",
|
||||
onUpdate: function () { u.apply(this.targets()[0].v); },
|
||||
onComplete: () => wc(u, false) }); }
|
||||
});
|
||||
}
|
||||
});
|
||||
ScrollTrigger.refresh();
|
||||
};
|
||||
// the aperitif terminal types its transcript once, when it first enters view
|
||||
document.querySelectorAll(".crt").forEach(crt => {
|
||||
@@ -1002,7 +998,7 @@ body:not(.fx) .slab{display:none}
|
||||
io.observe(crt);
|
||||
});
|
||||
|
||||
Promise.all(units.map(u => u.flat.decode ? u.flat.decode().catch(() => {}) : Promise.resolve()))
|
||||
.then(() => requestAnimationFrame(() => requestAnimationFrame(boot)));
|
||||
// boot on the next frame; do not gate on decoding lazy images (that stalls with loading=lazy)
|
||||
requestAnimationFrame(() => requestAnimationFrame(boot));
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user