Files
exploded-cluster/ec2-part2.html
T

253 lines
11 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
(() => {
"use strict";
const RM = matchMedia("(prefers-reduced-motion: reduce)").matches;
const heroPin = document.getElementById("hero-pin");
const crt = document.getElementById("crt");
const crtbody = document.getElementById("crtbody");
const caret = document.getElementById("caret");
const cue = document.getElementById("cue");
const skip = document.getElementById("skip");
const titlewrap = document.querySelector(".titlewrap");
/* ---------- split the big title into 3D chars ---------- */
document.querySelectorAll("#bigtitle .x1, #bigtitle .x2").forEach(el => {
const t = el.textContent; el.textContent = "";
[...t].forEach(c => {
const s = document.createElement("span");
s.className = "ch"; s.textContent = c === " " ? " " : c;
el.appendChild(s);
});
});
const chars = document.querySelectorAll("#bigtitle .ch");
/* ---------- terminal script ---------- */
const LINES = [
["p1", "jonny@edge:~$ ", "docker build -t shop/app:0723R1 ."],
["dm", " => [1/4] FROM ubi9-minimal", " CACHED"],
["dm", " => [2/4] RUN microdnf install deps", " 0.8s"],
["dm", " => [3/4] COPY src/ /app", " 0.1s"],
["dm", " => [4/4] USER 10001", " done"],
["cy", " => exporting layers ", "sha256:9f2c11ab…"],
["p1", "jonny@edge:~$ ", "docker push shop/app:0723R1"],
["p1", "jonny@edge:~$ ", "kubectl apply -f app.yaml"],
["ok", "deployment.apps/shop-app created", ""],
["ok", "service/shop-app created", ""],
["p1", "jonny@edge:~$ ", "kubectl get pods -w"],
["dm", "shop-app-7d4b9c-x2x3g 0/1 ContainerCreating", ""],
["ok", "shop-app-7d4b9c-x2x3g 1/1 Running", ""],
["mg", "…but what actually just happened?", ""]
];
let li = 0, done = false;
function renderAll() { // instant final state (skip / reduced motion / no theatre)
crtbody.innerHTML = "";
LINES.forEach(L => {
const ln = document.createElement("span");
ln.className = "ln";
ln.innerHTML = '<span class="' + L[0] + '"></span><span></span>';
ln.children[0].textContent = L[1]; ln.children[1].textContent = L[2];
crtbody.appendChild(ln);
});
crtbody.appendChild(caret);
finishIntro();
}
function typeLine() {
if (done) return;
if (li >= LINES.length) { finishIntro(); return; }
const L = LINES[li++];
const ln = document.createElement("span");
ln.className = "ln";
const a = document.createElement("span"); a.className = L[0]; a.textContent = L[1];
const b = document.createElement("span");
ln.appendChild(a); ln.appendChild(b);
crtbody.insertBefore(ln, caret);
const isCmd = L[0] === "p1";
if (!isCmd) { b.textContent = L[2]; setTimeout(typeLine, 170); return; }
let i = 0;
(function tick() {
if (done) return;
b.textContent = L[2].slice(0, ++i);
if (i < L[2].length) setTimeout(tick, 26);
else setTimeout(typeLine, 320);
})();
}
function finishIntro() {
if (done) return; done = true;
skip.style.display = "none";
cue.style.opacity = 1;
}
skip.addEventListener("click", () => { done = false; renderAll(); });
/* ---------- tooltip (hover / focus / tap) ---------- */
const tip = document.getElementById("tip");
let tipFor = null;
function showTip(el) {
tipFor = el;
tip.innerHTML = "<b></b>";
tip.firstChild.textContent = el.textContent.trim();
tip.appendChild(document.createTextNode(el.dataset.def));
tip.classList.add("show"); tip.setAttribute("aria-hidden", "false");
const r = el.getBoundingClientRect(), tw = tip.offsetWidth, th = tip.offsetHeight;
let x = Math.max(10, Math.min(r.left + r.width / 2 - tw / 2, innerWidth - tw - 10));
let y = r.top - th - 10; if (y < 10) y = r.bottom + 10;
tip.style.left = x + "px"; tip.style.top = y + "px";
}
function hideTip() { tipFor = null; tip.classList.remove("show"); tip.setAttribute("aria-hidden", "true"); }
document.querySelectorAll(".term").forEach(el => {
el.addEventListener("mouseenter", () => showTip(el));
el.addEventListener("mouseleave", hideTip);
el.addEventListener("focus", () => showTip(el));
el.addEventListener("blur", hideTip);
el.addEventListener("click", ev => { ev.preventDefault();
(tipFor === el && tip.classList.contains("show")) ? hideTip() : showTip(el); });
});
addEventListener("scroll", () => { if (tipFor) hideTip(); }, { passive: true });
/* ---------- progress bar ---------- */
const bar = document.getElementById("bar");
let raf = 0;
addEventListener("scroll", () => {
if (raf) return;
raf = requestAnimationFrame(() => {
const h = document.documentElement;
bar.style.transform = "scaleX(" + (h.scrollTop / (h.scrollHeight - h.clientHeight || 1)) + ")";
raf = 0;
});
}, { passive: true });
/* ---------- section reveal + nav (IO, all contexts) ---------- */
const navLinks = document.querySelectorAll("nav a");
const io = new IntersectionObserver(ents => ents.forEach(e => {
if (!e.isIntersecting) return;
e.target.classList.add("live");
const id = e.target.id;
if (id) navLinks.forEach(a => a.classList.toggle("on", a.getAttribute("href") === "#" + id));
}), { threshold: 0.3 });
document.querySelectorAll("section").forEach(s => io.observe(s));
/* ================= THE THEATRE (motion-safe contexts only) ================= */
if (RM || typeof gsap === "undefined") { renderAll(); document.body.classList.remove("fx"); return; }
document.body.classList.add("fx");
gsap.registerPlugin(ScrollTrigger);
ScrollTrigger.config({ ignoreMobileResize: true });
/* particles: capped starfield with scroll parallax */
const cv = document.getElementById("stars"), cx = cv.getContext("2d");
let W, H, DPR, stars = [];
function sizeCv() {
DPR = Math.min(devicePixelRatio || 1, 2);
W = cv.width = innerWidth * DPR; H = cv.height = innerHeight * DPR;
cv.style.width = innerWidth + "px"; cv.style.height = innerHeight + "px";
stars = Array.from({ length: innerWidth < 720 ? 46 : 90 }, () => ({
x: Math.random() * W, y: Math.random() * H,
z: .3 + Math.random() * .7, r: (.6 + Math.random() * 1.5) * DPR,
tw: Math.random() * Math.PI * 2, c: Math.random() < .12 ? "232,121,249" : "63,186,245"
}));
}
sizeCv(); addEventListener("resize", sizeCv);
let lastY = 0, running = true;
document.addEventListener("visibilitychange", () => { running = !document.hidden; if (running) loop(); });
function loop(t) {
if (!running) return;
const dy = (scrollY - lastY) * .18; lastY = scrollY;
cx.clearRect(0, 0, W, H);
for (const s of stars) {
s.y -= dy * s.z * DPR; s.tw += .02;
if (s.y < -8) s.y += H + 16; else if (s.y > H + 8) s.y -= H + 16;
cx.globalAlpha = .25 + .45 * s.z * (0.7 + 0.3 * Math.sin(s.tw));
cx.fillStyle = "rgba(" + s.c + ",1)";
cx.beginPath(); cx.arc(s.x, s.y, s.r * s.z, 0, 7); cx.fill();
}
cx.globalAlpha = 1;
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
/* card tilt (hover-capable pointers only) */
if (matchMedia("(hover:hover)").matches) {
let tiltRaf = 0;
document.addEventListener("pointermove", ev => {
const card = ev.target.closest(".card"); if (!card) return;
if (tiltRaf) return;
tiltRaf = requestAnimationFrame(() => {
const r = card.getBoundingClientRect();
const px = (ev.clientX - r.left) / r.width, py = (ev.clientY - r.top) / r.height;
card.style.setProperty("--ry", ((px - .5) * 16).toFixed(2) + "deg");
card.style.setProperty("--rx", ((.5 - py) * 12).toFixed(2) + "deg");
card.style.setProperty("--gx", (px * 100).toFixed(1) + "%");
card.style.setProperty("--gy", (py * 100).toFixed(1) + "%");
tiltRaf = 0;
});
}, { passive: true });
document.addEventListener("pointerout", ev => {
const card = ev.target.closest(".card"); if (!card) return;
card.style.setProperty("--rx", "0deg"); card.style.setProperty("--ry", "0deg");
}, { passive: true });
}
/* start typing the terminal */
gsap.set(titlewrap, { autoAlpha: 0 });
gsap.set(chars, { z: -600, autoAlpha: 0, rotateX: -70 });
setTimeout(typeLine, 500);
/* COURSE 00 — pin: terminal tilts back, flies apart; title chars fly in from depth */
const mm = gsap.matchMedia();
mm.add("(min-width: 721px)", () => {
gsap.timeline({
scrollTrigger: { trigger: heroPin, start: "top top", end: "+=1600", scrub: .6, pin: true }
})
.to(crt, { rotateX: 38, z: -180, y: -60, ease: "power1.in" }, 0)
.to(crt, { autoAlpha: 0, scale: 1.25, rotateX: 62, y: -220, ease: "power2.in", duration: .6 }, .35)
.to(cue, { autoAlpha: 0, duration: .15 }, 0)
.to("#skip", { autoAlpha: 0, duration: .1 }, 0)
.to(titlewrap, { autoAlpha: 1, duration: .2 }, .45)
.to(chars, { z: 0, autoAlpha: 1, rotateX: 0, stagger: { each: .012, from: "random" }, ease: "back.out(1.4)" }, .5)
.to("#bigtitle", { scale: .96, y: -14, ease: "none" }, .9);
/* COURSE 01 — pin: layers rise from the deep into the tilted stack */
const layers = gsap.utils.toArray(".layer3d");
gsap.set(layers, { z: -650, autoAlpha: 0, y: 260 });
gsap.set(".laycard", { autoAlpha: 0, y: 40 });
gsap.timeline({
scrollTrigger: { trigger: "#lay-stage", start: "top top", end: "+=1800", scrub: .6, pin: true }
})
.to(layers[0], { z: 0, autoAlpha: 1, y: 150, ease: "power2.out" })
.to(layers[1], { z: 46, autoAlpha: 1, y: 75, ease: "power2.out" })
.to(layers[2], { z: 92, autoAlpha: 1, y: 0, ease: "power2.out" })
.to(layers[3], { z: 138, autoAlpha: 1, y: -75, ease: "power2.out" })
.to("#laystack", { rotateX: 24, rotateZ: -6, ease: "none" }, "<")
.to(".laycard", { autoAlpha: 1, y: 0, stagger: .15 })
.to("#laystack", { rotateX: 10, rotateZ: 0, ease: "none" });
/* COURSE 05 — pin: the ring of opinions spins around the core */
gsap.timeline({
scrollTrigger: { trigger: "#orbit-stage", start: "top top", end: "+=2000", scrub: .7, pin: true }
})
.fromTo("#orbit", { rotateY: -15, rotateX: 8 }, { rotateY: 285, rotateX: 8, ease: "none" }, 0)
.fromTo("#core > .card", { scale: .7, autoAlpha: 0 }, { scale: 1, autoAlpha: 1, duration: .12 }, 0);
return () => {}; /* matchMedia cleanup handled by GSAP */
});
/* small screens: no pins — everything just assembles via IO; terminal still types */
mm.add("(max-width: 720px)", () => {
gsap.set(titlewrap, { autoAlpha: 0 });
ScrollTrigger.create({
trigger: heroPin, start: "bottom 70%",
onEnter: () => {
gsap.to(crt, { autoAlpha: 0, y: -80, duration: .5 });
gsap.to(titlewrap, { autoAlpha: 1, duration: .5 });
gsap.to(chars, { z: 0, autoAlpha: 1, rotateX: 0, stagger: .01, duration: .6 });
}
});
gsap.set(".layer3d", { clearProps: "all" });
gsap.set(".laycard", { clearProps: "all" });
gsap.fromTo("#orbit", { rotateY: -15 }, {
rotateY: 285, ease: "none",
scrollTrigger: { trigger: "#orbit-stage", start: "top 80%", end: "bottom 20%", scrub: .7 }
});
return () => {};
});
})();
</script>