v2 source, NB2 prompt contract, and the slice editor

This commit is contained in:
2026-07-27 10:59:22 +10:00
commit e45f5729fa
10 changed files with 2200 additions and 0 deletions
+252
View File
@@ -0,0 +1,252 @@
<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>