Members / script.js
wop's picture
Create script.js
e01f393 verified
Raw
History Blame Contribute Delete
11.7 kB
(() => {
document.addEventListener("DOMContentLoaded", () => {
const bg = document.querySelector(".background");
// ---- Inject realistic clouds into .background ----
if (bg && !bg.querySelector(".cloud")) {
const isMobile = window.matchMedia("(max-width:768px)").matches;
if (!isMobile && !document.getElementById("cloud-fluff")) {
const defs = document.createElementNS("http://www.w3.org/2000/svg", "svg");
defs.setAttribute("width", "0");
defs.setAttribute("height", "0");
defs.setAttribute("aria-hidden", "true");
defs.style.position = "absolute";
defs.innerHTML =
'<filter id="cloud-fluff" x="-150%" y="-150%" width="400%" height="400%">' +
'<feTurbulence type="fractalNoise" baseFrequency="0.012 0.02" numOctaves="4" seed="11" result="noise"/>' +
'<feDisplacementMap in="SourceGraphic" in2="noise" scale="120"/>' +
"</filter>";
document.body.appendChild(defs);
}
const cloudCount = isMobile ? 2 : 4;
for (let i = 1; i <= cloudCount; i++) {
const cloud = document.createElement("div");
cloud.className = "cloud cloud-" + i;
cloud.setAttribute("aria-hidden", "true");
bg.appendChild(cloud);
}
}
// ---- Grass strip along the bottom of the viewport ----
if (!document.querySelector(".grass")) {
const NS = "http://www.w3.org/2000/svg";
const W = isMobile ? 1200 : 2400;
const H = 70;
const svg = document.createElementNS(NS, "svg");
svg.setAttribute("class", "grass");
svg.setAttribute("viewBox", "0 0 " + W + " " + H);
svg.setAttribute("preserveAspectRatio", "xMidYMax slice");
svg.setAttribute("aria-hidden", "true");
// soft ground shading under the blades
svg.innerHTML =
'<defs><linearGradient id="grass-base" x1="0" y1="1" x2="0" y2="0">' +
'<stop offset="0" stop-color="#3e7a46" stop-opacity="0.55"/>' +
'<stop offset="1" stop-color="#3e7a46" stop-opacity="0"/>' +
"</linearGradient></defs>" +
'<rect x="0" y="' + (H - 16) + '" width="' + W + '" height="16" fill="url(#grass-base)"/>';
const isMobile = window.matchMedia("(max-width: 768px)").matches;
const layers = isMobile
? [
{ count: 50, hMin: 18, hMax: 32, colors: ["#4f9854"], opacity: 0.9 },
{ count: 40, hMin: 24, hMax: 42, colors: ["#63ad64"], opacity: 1 },
]
: [
{ count: 190, hMin: 18, hMax: 36, colors: ["#3e7a46","#457f4b","#4a8a50"], opacity: 0.8 },
{ count: 160, hMin: 26, hMax: 52, colors: ["#4f9854","#57a05a","#63ad64","#6db36e"], opacity: 1 },
];
const layerEls = [];
layers.forEach((layer) => {
const g = document.createElementNS(NS, "g");
g.setAttribute("opacity", layer.opacity);
g.style.transformBox = "fill-box";
g.style.transformOrigin = "50% 100%";
layerEls.push(g);
for (let i = 0; i < layer.count; i++) {
const x = Math.random() * W;
const h = layer.hMin + Math.random() * (layer.hMax - layer.hMin);
const lean = (Math.random() - 0.5) * 16;
const w = 3 + Math.random() * 3;
const color = layer.colors[(Math.random() * layer.colors.length) | 0];
const blade = document.createElementNS(NS, "path");
blade.setAttribute(
"d",
"M " + x + " " + H +
" Q " + (x + lean * 0.2) + " " + (H - h * 0.6) + " " + (x + lean) + " " + (H - h) +
" Q " + (x + lean * 0.6 + w * 0.5) + " " + (H - h * 0.55) + " " + (x + w) + " " + H + " Z"
);
blade.setAttribute("fill", color);
blade.style.transformBox = "fill-box";
blade.style.transformOrigin = "50% 100%";
// stagger by x so a gust appears to travel across the lawn
if (!isMobile) {
blade.style.animation =
"blade-sway " + (2.6 + Math.random() * 2.4).toFixed(2) + "s ease-in-out " +
(-(x / W) * 3 - Math.random()).toFixed(2) + "s infinite alternate";
}
g.appendChild(blade);
}
svg.appendChild(g);
});
document.body.appendChild(svg);
// ---- Scroll physics: scrolling down squashes the grass toward the
// ground, scrolling up stretches it, and a damped spring makes it
// boing back upright ----
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
let squash = 0; // offset from natural height (scaleY = 1 + squash)
let squashVel = 0;
let lastY = window.scrollY;
let raf = null;
const settle = () => {
squashVel -= squash * 0.06; // spring pull toward natural height
squashVel *= 0.88; // damping
squash += squashVel;
const s = Math.max(-0.3, Math.min(0.14, squash));
layerEls.forEach((g, i) => {
// back layer reacts less than the front for depth
const depth = i === 0 ? 0.55 : 1;
g.style.transform = "scaleY(" + (1 + s * depth).toFixed(4) + ")";
});
if (Math.abs(squash) > 0.001 || Math.abs(squashVel) > 0.001) {
raf = requestAnimationFrame(settle);
} else {
layerEls.forEach((g) => (g.style.transform = ""));
raf = null;
}
};
const impulse = (v) => {
squashVel += v;
if (!raf) raf = requestAnimationFrame(settle);
};
window.addEventListener(
"scroll",
() => {
const y = window.scrollY;
// scroll down -> stretch, scroll up -> duck
impulse(Math.max(-0.08, Math.min(0.08, (y - lastY) * 0.0015)));
lastY = y;
},
{ passive: true }
);
// wheel fallback so short pages that can't scroll still react
window.addEventListener(
"wheel",
(e) => {
if (document.documentElement.scrollHeight <= window.innerHeight + 4) {
impulse(Math.max(-0.05, Math.min(0.05, e.deltaY * 0.0005)));
}
},
{ passive: true }
);
}
}
// ---- Copy buttons on <pre> ----
document.querySelectorAll("pre").forEach((pre) => {
if (pre.querySelector(".copy-btn")) return;
const code = pre.querySelector("code") || pre;
const btn = document.createElement("button");
btn.className = "copy-btn";
btn.textContent = "Copy";
btn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(code.innerText);
btn.textContent = "Copied!";
setTimeout(() => (btn.textContent = "Copy"), 1200);
} catch {
btn.textContent = "Error";
setTimeout(() => (btn.textContent = "Copy"), 1200);
}
});
pre.appendChild(btn);
});
// ---- Article-only features: sidebar + collapsible code ----
const article = document.querySelector(".article");
if (!article) return;
// Collect headings for TOC
const headings = article.querySelectorAll("h1, h2, h3");
const codeBlocks = article.querySelectorAll("pre");
// Determine filenames from preceding h2
const codeFiles = [];
codeBlocks.forEach((pre, idx) => {
let name = null;
let el = pre.previousElementSibling;
while (el) {
if (el.tagName === "H2" || el.tagName === "H3") {
const text = el.textContent.trim();
if (text.match(/\.\w{1,5}$/) || text.match(/\.\w{1,5}\s/)) {
name = text.replace(/\s*\(.*\)/, "").trim();
}
break;
}
el = el.previousElementSibling;
}
if (!name) name = `code-block-${idx + 1}.txt`;
codeFiles.push({ name, pre });
});
// Make code blocks collapsible
codeBlocks.forEach((pre, idx) => {
const wrapper = document.createElement("div");
wrapper.className = "code-collapsible";
const toggle = document.createElement("button");
toggle.className = "code-toggle";
const fileName = codeFiles[idx].name;
toggle.innerHTML = `<span class="code-toggle-arrow">▶</span> <span class="code-toggle-name">${fileName}</span>`;
toggle.setAttribute("aria-expanded", "false");
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(toggle);
wrapper.appendChild(pre);
pre.classList.add("collapsed");
toggle.addEventListener("click", () => {
const expanded = pre.classList.toggle("collapsed");
toggle.setAttribute("aria-expanded", !expanded);
toggle.querySelector(".code-toggle-arrow").textContent = expanded ? "▶" : "▼";
});
});
// Build sidebar
const sidebar = document.createElement("nav");
sidebar.className = "article-sidebar";
// TOC section
const tocTitle = document.createElement("div");
tocTitle.className = "sidebar-title";
tocTitle.textContent = "Contents";
sidebar.appendChild(tocTitle);
const tocList = document.createElement("ul");
tocList.className = "sidebar-toc";
headings.forEach((h, i) => {
if (!h.id) h.id = "heading-" + i;
const li = document.createElement("li");
li.className = "toc-" + h.tagName.toLowerCase();
const a = document.createElement("a");
a.href = "#" + h.id;
a.textContent = h.textContent;
a.addEventListener("click", (e) => {
e.preventDefault();
h.scrollIntoView({ behavior: "smooth", block: "start" });
});
li.appendChild(a);
tocList.appendChild(li);
});
sidebar.appendChild(tocList);
// Files section
if (codeFiles.length > 0) {
const filesTitle = document.createElement("div");
filesTitle.className = "sidebar-title";
filesTitle.textContent = "Files";
sidebar.appendChild(filesTitle);
const fileList = document.createElement("ul");
fileList.className = "sidebar-files";
codeFiles.forEach(({ name, pre }) => {
const li = document.createElement("li");
const btn = document.createElement("button");
btn.className = "sidebar-file-btn";
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> ${name}`;
btn.addEventListener("click", () => {
const code = pre.querySelector("code") || pre;
const text = code.innerText;
const blob = new Blob([text], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = name;
a.click();
URL.revokeObjectURL(url);
});
li.appendChild(btn);
fileList.appendChild(li);
});
sidebar.appendChild(fileList);
}
document.body.appendChild(sidebar);
// Highlight active TOC item on scroll
const tocLinks = tocList.querySelectorAll("a");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
tocLinks.forEach((l) => l.classList.remove("active"));
const link = tocList.querySelector(`a[href="#${entry.target.id}"]`);
if (link) link.classList.add("active");
}
});
},
{ rootMargin: "-80px 0px -70% 0px" }
);
headings.forEach((h) => observer.observe(h));
});
})();