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:
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -20,7 +20,8 @@ for f in pilot/arc.tpl.html pilot/manifest-docker.json pilot/manifest-cluster.js
|
|||||||
assets/dist/delivery-flow.svg assets/dist/docker-layers.webp \
|
assets/dist/delivery-flow.svg assets/dist/docker-layers.webp \
|
||||||
assets/dist/course-II-cluster.webp assets/dist/course-00-surround.webp \
|
assets/dist/course-II-cluster.webp assets/dist/course-00-surround.webp \
|
||||||
assets/dist/course-III-node.webp assets/dist/course-III-pod.webp \
|
assets/dist/course-III-node.webp assets/dist/course-III-pod.webp \
|
||||||
assets/dist/course-IV-traffic.webp assets/dist/course-V-orbit.webp; do
|
assets/dist/course-IV-traffic.webp assets/dist/course-V-orbit.webp \
|
||||||
|
assets/dist/intro-establishing.webp; do
|
||||||
[[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; }
|
[[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
+61
-6
@@ -1,4 +1,5 @@
|
|||||||
<header class="masthead">
|
<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>
|
<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>
|
<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
|
<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}
|
.purpose h2.kt{margin:0 0 1rem}
|
||||||
b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cyan)}
|
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;
|
.eyebrow{font-family:var(--mono);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
|
||||||
color:var(--cyan);margin:0 0 1.5rem}
|
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;
|
.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}
|
gap:26px;align-items:center}
|
||||||
.scene{position:relative;margin:0;line-height:0}
|
.scene{position:relative;margin:0;line-height:0}
|
||||||
.scene .flat{display:block;width:100%;height:auto}
|
.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{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}
|
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 .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}
|
.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 flat = scene.querySelector(".flat");
|
||||||
const legend = sec.querySelector(".legend");
|
const legend = sec.querySelector(".legend");
|
||||||
// legend text comes from repo JSON; build with DOM nodes + textContent, never innerHTML
|
// 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;
|
if (!p.title && !p.blurb) return;
|
||||||
const li = document.createElement("li");
|
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 t = document.createElement("p"); t.className = "t"; t.textContent = p.title;
|
||||||
const b = document.createElement("p"); b.className = "b"; b.textContent = p.blurb;
|
const b = document.createElement("p"); b.className = "b"; b.textContent = p.blurb;
|
||||||
li.append(t, b); legend.appendChild(li);
|
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).
|
// Reduced motion / no GSAP: leave the flat exploded hero in place (it IS the end state).
|
||||||
if (RM || !window.gsap || !window.ScrollTrigger) return;
|
if (RM || !window.gsap || !window.ScrollTrigger) return;
|
||||||
document.body.classList.add("fx");
|
document.body.classList.add("fx");
|
||||||
@@ -1002,8 +1028,37 @@ body:not(.fx) .slab{display:none}
|
|||||||
"translate3d(" + ox.toFixed(3) + "%," + oy.toFixed(3) + "%,0)" + extra;
|
"translate3d(" + ox.toFixed(3) + "%," + oy.toFixed(3) + "%,0)" + extra;
|
||||||
});
|
});
|
||||||
u.scene.style.transform = "scale(" + (0.95 + 0.05 * t).toFixed(4) + ")";
|
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 = () => {
|
const boot = () => {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ let app = tpl.slice(i + 8, j)
|
|||||||
let body = tpl.slice(0, i) + tpl.slice(j + 9);
|
let body = tpl.slice(0, i) + tpl.slice(j + 9);
|
||||||
|
|
||||||
const heroes = {
|
const heroes = {
|
||||||
|
__HERO_INTRO__: "intro-establishing.webp",
|
||||||
__HERO_SURROUND__: "course-00-surround.webp",
|
__HERO_SURROUND__: "course-00-surround.webp",
|
||||||
__HERO_NODE__: "course-III-node.webp",
|
__HERO_NODE__: "course-III-node.webp",
|
||||||
__HERO_POD__: "course-III-pod.webp",
|
__HERO_POD__: "course-III-pod.webp",
|
||||||
@@ -90,8 +91,9 @@ for (const [ph, file] of Object.entries(heroes)) {
|
|||||||
heroUrls[ph] = url;
|
heroUrls[ph] = url;
|
||||||
const dims = webpSize(buf);
|
const dims = webpSize(buf);
|
||||||
const dimAttr = dims ? ` width="${dims.w}" height="${dims.h}"` : "";
|
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.
|
// The intro backdrop is the above-the-fold LCP candidate -> eager. Everything else,
|
||||||
const load = ph === "__HERO_SURROUND__" ? "eager" : "lazy";
|
// 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}"`);
|
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_GITOPS__", man("gitops")) \
|
||||||
.replace("__MAN_SUPPLY__", man("supply")) \
|
.replace("__MAN_SUPPLY__", man("supply")) \
|
||||||
.replace("__MAN_HELM__", man("helm")) \
|
.replace("__MAN_HELM__", man("helm")) \
|
||||||
|
.replace("__HERO_INTRO__", b64("intro-establishing.webp")) \
|
||||||
.replace("__HERO_SURROUND__", b64("course-00-surround.webp")) \
|
.replace("__HERO_SURROUND__", b64("course-00-surround.webp")) \
|
||||||
.replace("__HERO_NODE__", b64("course-III-node.webp")) \
|
.replace("__HERO_NODE__", b64("course-III-node.webp")) \
|
||||||
.replace("__HERO_POD__", b64("course-III-pod.webp")) \
|
.replace("__HERO_POD__", b64("course-III-pod.webp")) \
|
||||||
|
|||||||
Reference in New Issue
Block a user