intro backdrop + object-text linkage: establishing shot anchored below the copy (parallax, disabled under reduced motion); each legend row focuses its own part - the scrub walks the list and the described object stands out while the rest sit back, hover/keyboard pins the link
This commit is contained in:
+61
-6
@@ -1,4 +1,5 @@
|
||||
<header class="masthead">
|
||||
<div class="introbg" aria-hidden="true"><img src="__HERO_INTRO__" alt=""></div>
|
||||
<p class="eyebrow">The Exploded Cluster · The Delivery Arc</p>
|
||||
<h1>The machine was the easy part.<br><em>Now watch how software reaches it.</em></h1>
|
||||
<p class="lede">The foundations first - what an image is, what a cluster is - then the toolchain
|
||||
@@ -782,7 +783,15 @@ a:hover,a:focus-visible{border-bottom-color:var(--cyan)}
|
||||
.purpose h2.kt{margin:0 0 1rem}
|
||||
b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cyan)}
|
||||
|
||||
.masthead{max-width:64ch;margin:0 auto;padding:16vh 24px 9vh;text-align:center}
|
||||
.masthead{position:relative;max-width:64ch;margin:0 auto;padding:16vh 24px 9vh;text-align:center}
|
||||
/* The archipelago is anchored BELOW the text block: the words never sit over machinery.
|
||||
Top edge starts under the lede; the mask feathers both ends so it dissolves into the void. */
|
||||
.introbg{position:absolute;inset:52% -50vw -2vh;z-index:-1;overflow:hidden;pointer-events:none}
|
||||
.introbg img{width:100%;height:100%;object-fit:cover;object-position:center 62%;opacity:.55;
|
||||
-webkit-mask-image:linear-gradient(180deg,transparent 0%,#000 26%,#000 82%,transparent 100%);
|
||||
mask-image:linear-gradient(180deg,transparent 0%,#000 26%,#000 82%,transparent 100%)}
|
||||
@media (max-width:900px){.introbg{inset:58% -60vw -2vh}.introbg img{opacity:.4}}
|
||||
.masthead > :not(.introbg){position:relative}
|
||||
.eyebrow{font-family:var(--mono);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
|
||||
color:var(--cyan);margin:0 0 1.5rem}
|
||||
.masthead h1,.thesis{font-size:clamp(1.9rem,4.8vw,3.2rem);line-height:1.1;margin:0 0 1.3rem;
|
||||
@@ -831,12 +840,15 @@ b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cya
|
||||
gap:26px;align-items:center}
|
||||
.scene{position:relative;margin:0;line-height:0}
|
||||
.scene .flat{display:block;width:100%;height:auto}
|
||||
.slab{position:absolute;inset:0;background-size:100% 100%;background-repeat:no-repeat}
|
||||
.slab{position:absolute;inset:0;background-size:100% 100%;background-repeat:no-repeat;
|
||||
transition:opacity var(--dur-base) ease}
|
||||
|
||||
.legend{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:13px}
|
||||
.legend li{padding-left:22px;border-left:1px solid var(--line);
|
||||
.legend li{padding-left:22px;border-left:1px solid var(--line);cursor:default;
|
||||
opacity:.34;transition:opacity var(--dur-base) ease,border-color var(--dur-base) ease}
|
||||
.legend li.lit{opacity:1;border-left-color:var(--cyan)}
|
||||
.legend li.lit{opacity:.72}
|
||||
.legend li.act{opacity:1;border-left-color:var(--cyan)}
|
||||
.legend li:hover,.legend li:focus-visible{opacity:1}
|
||||
.legend .t{font-size:.92rem;font-weight:600;margin:0 0 .22rem;letter-spacing:-.01em}
|
||||
.legend .b{font-size:.76rem;line-height:1.5;color:var(--dim);margin:0}
|
||||
|
||||
@@ -919,9 +931,11 @@ body:not(.fx) .slab{display:none}
|
||||
const flat = scene.querySelector(".flat");
|
||||
const legend = sec.querySelector(".legend");
|
||||
// legend text comes from repo JSON; build with DOM nodes + textContent, never innerHTML
|
||||
if (legend) man.parts.forEach(p => {
|
||||
if (legend) man.parts.forEach((p, i) => {
|
||||
if (!p.title && !p.blurb) return;
|
||||
const li = document.createElement("li");
|
||||
li.dataset.part = String(i);
|
||||
li.tabIndex = 0; // keyboard users get the same link as the mouse
|
||||
const t = document.createElement("p"); t.className = "t"; t.textContent = p.title;
|
||||
const b = document.createElement("p"); b.className = "b"; b.textContent = p.blurb;
|
||||
li.append(t, b); legend.appendChild(li);
|
||||
@@ -953,6 +967,18 @@ body:not(.fx) .slab{display:none}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- intro backdrop (class: parallax) ------------------------------------------------
|
||||
// Drifts up and fades as the index arrives. Parallax is disabled outright under reduced
|
||||
// motion (house rule), which is why this sits after the RM check for its own animation.
|
||||
const introBg = document.querySelector(".introbg img");
|
||||
if (introBg && !RM && window.gsap && window.ScrollTrigger) {
|
||||
gsap.to(introBg, {
|
||||
yPercent: -12, opacity: 0.12, ease: TOK.EASE.scrub,
|
||||
scrollTrigger: { trigger: ".masthead", start: "top top", end: "bottom top",
|
||||
scrub: TOK.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");
|
||||
@@ -1002,8 +1028,37 @@ body:not(.fx) .slab{display:none}
|
||||
"translate3d(" + ox.toFixed(3) + "%," + oy.toFixed(3) + "%,0)" + extra;
|
||||
});
|
||||
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)));
|
||||
// Object <-> text linkage: the row being read is the part that stands out. The scrub
|
||||
// walks the list top to bottom; whichever row is current gets .act, its slab stays at
|
||||
// full strength and the rest sit back. Reading position and visual focus never diverge.
|
||||
if (n) {
|
||||
let act = -1;
|
||||
lis.forEach((li, i) => {
|
||||
const on = t >= 0.9 * ((i + 0.5) / n);
|
||||
li.classList.toggle("lit", on);
|
||||
if (on) act = i;
|
||||
});
|
||||
u.act = act;
|
||||
if (u.pin == null) u.paint();
|
||||
}
|
||||
};
|
||||
// paint() applies the focus state: pinned (hover/focus) wins over the scroll position.
|
||||
u.paint = () => {
|
||||
const f = u.pin != null ? u.pin : u.act;
|
||||
u.slabs.forEach((el, i) => { el.style.opacity = (f < 0 || i === f) ? "1" : "0.38"; });
|
||||
lis.forEach((li, i) => li.classList.toggle("act", i === f));
|
||||
};
|
||||
u.pin = null; u.act = -1;
|
||||
// hover or keyboard focus on a row pins the link to that part - user-initiated, so it
|
||||
// stays available even when the scrub is not moving.
|
||||
lis.forEach((li, i) => {
|
||||
const on = () => { u.pin = i; u.paint(); };
|
||||
const off = () => { u.pin = null; u.paint(); };
|
||||
li.addEventListener("mouseenter", on);
|
||||
li.addEventListener("focus", on);
|
||||
li.addEventListener("mouseleave", off);
|
||||
li.addEventListener("blur", off);
|
||||
});
|
||||
});
|
||||
|
||||
const boot = () => {
|
||||
|
||||
@@ -60,6 +60,7 @@ let app = tpl.slice(i + 8, j)
|
||||
let body = tpl.slice(0, i) + tpl.slice(j + 9);
|
||||
|
||||
const heroes = {
|
||||
__HERO_INTRO__: "intro-establishing.webp",
|
||||
__HERO_SURROUND__: "course-00-surround.webp",
|
||||
__HERO_NODE__: "course-III-node.webp",
|
||||
__HERO_POD__: "course-III-pod.webp",
|
||||
@@ -90,8 +91,9 @@ for (const [ph, file] of Object.entries(heroes)) {
|
||||
heroUrls[ph] = url;
|
||||
const dims = webpSize(buf);
|
||||
const dimAttr = dims ? ` width="${dims.w}" height="${dims.h}"` : "";
|
||||
// Course 00's hero is the first visible image -> load eager; the rest are below the fold.
|
||||
const load = ph === "__HERO_SURROUND__" ? "eager" : "lazy";
|
||||
// The intro backdrop is the above-the-fold LCP candidate -> eager. Everything else,
|
||||
// including Course 00's hero, sits below the fold and lazy-loads.
|
||||
const load = ph === "__HERO_INTRO__" ? "eager" : "lazy";
|
||||
body = body.replace(`src="${ph}"`, `src="${url}"${dimAttr} decoding="async" loading="${load}"`);
|
||||
}
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ out = (ROOT / "pilot/arc.tpl.html").read_text() \
|
||||
.replace("__MAN_GITOPS__", man("gitops")) \
|
||||
.replace("__MAN_SUPPLY__", man("supply")) \
|
||||
.replace("__MAN_HELM__", man("helm")) \
|
||||
.replace("__HERO_INTRO__", b64("intro-establishing.webp")) \
|
||||
.replace("__HERO_SURROUND__", b64("course-00-surround.webp")) \
|
||||
.replace("__HERO_NODE__", b64("course-III-node.webp")) \
|
||||
.replace("__HERO_POD__", b64("course-III-pod.webp")) \
|
||||
|
||||
Reference in New Issue
Block a user