179 lines
5 KiB
JavaScript
179 lines
5 KiB
JavaScript
(() => {
|
|
const typewordEl = document.getElementById("typeword");
|
|
const stepperEl = document.getElementById("stepper");
|
|
const revealStepper = createStepperRevealer(stepperEl);
|
|
if (stepperEl) window.setTimeout(revealStepper, 5200);
|
|
|
|
if (typewordEl) startTypewriter(typewordEl, { onFirstCycleDone: revealStepper });
|
|
|
|
const quoteTextEl = document.getElementById("quoteText");
|
|
const quoteMetaEl = document.getElementById("quoteMeta");
|
|
const quoteWrapEl = quoteTextEl?.closest?.(".quote");
|
|
if (quoteTextEl && quoteMetaEl && quoteWrapEl) startQuoteTicker({ quoteWrapEl, quoteTextEl, quoteMetaEl });
|
|
})();
|
|
|
|
function createStepperRevealer(stepperEl) {
|
|
let shown = false;
|
|
return () => {
|
|
if (shown || !stepperEl) return;
|
|
shown = true;
|
|
stepperEl.hidden = false;
|
|
window.requestAnimationFrame(() => stepperEl.classList.add("homeStepper--show"));
|
|
};
|
|
}
|
|
|
|
function startTypewriter(typewordEl, opts = {}) {
|
|
const words = ["Transparent", "Traceable", "Trust", "TTT"];
|
|
const typeMs = 42;
|
|
const deleteMs = 26;
|
|
const holdMs = 780;
|
|
const finalHoldMs = 1100;
|
|
const betweenMs = 180;
|
|
const onFirstCycleDone = typeof opts.onFirstCycleDone === "function" ? opts.onFirstCycleDone : null;
|
|
|
|
let wordIndex = 0;
|
|
let charIndex = 0;
|
|
let isDeleting = false;
|
|
let cycleCount = 0;
|
|
|
|
const tick = () => {
|
|
const word = words[wordIndex] || "";
|
|
if (!isDeleting) {
|
|
charIndex = Math.min(word.length, charIndex + 1);
|
|
} else {
|
|
charIndex = Math.max(0, charIndex - 1);
|
|
}
|
|
|
|
typewordEl.textContent = word.slice(0, charIndex);
|
|
|
|
const atEnd = !isDeleting && charIndex === word.length;
|
|
const atStart = isDeleting && charIndex === 0;
|
|
|
|
if (atEnd) {
|
|
isDeleting = true;
|
|
const wait = wordIndex === words.length - 1 ? finalHoldMs : holdMs;
|
|
window.setTimeout(tick, wait);
|
|
return;
|
|
}
|
|
|
|
if (atStart) {
|
|
isDeleting = false;
|
|
wordIndex = (wordIndex + 1) % words.length;
|
|
if (wordIndex === 0) {
|
|
cycleCount += 1;
|
|
if (cycleCount === 1 && onFirstCycleDone) onFirstCycleDone();
|
|
}
|
|
window.setTimeout(tick, betweenMs);
|
|
return;
|
|
}
|
|
|
|
window.setTimeout(tick, isDeleting ? deleteMs : typeMs);
|
|
};
|
|
|
|
tick();
|
|
}
|
|
|
|
async function startQuoteTicker({ quoteWrapEl, quoteTextEl, quoteMetaEl }) {
|
|
const quotes = await loadQuotes();
|
|
if (!Array.isArray(quotes) || quotes.length === 0) return;
|
|
|
|
quoteWrapEl.classList.add("quote--show");
|
|
|
|
let idx = Math.floor(Math.random() * quotes.length);
|
|
|
|
const show = (q) => {
|
|
quoteWrapEl.classList.remove("quote--show");
|
|
quoteWrapEl.classList.add("quote--fade");
|
|
|
|
window.setTimeout(() => {
|
|
quoteTextEl.textContent = q.text || "";
|
|
renderQuoteMeta({ quoteMetaEl, q });
|
|
|
|
quoteWrapEl.classList.remove("quote--fade");
|
|
quoteWrapEl.classList.add("quote--show");
|
|
}, 220);
|
|
};
|
|
|
|
const loop = () => {
|
|
const q = quotes[idx] || {};
|
|
show(q);
|
|
idx = (idx + 1) % quotes.length;
|
|
|
|
const duration = estimateReadMs(q.text || "");
|
|
window.setTimeout(loop, duration);
|
|
};
|
|
|
|
loop();
|
|
}
|
|
|
|
async function loadQuotes() {
|
|
try {
|
|
const resp = await fetch(resolveIfTttUrl("assets/ifttt-quotes.json"), { cache: "no-store" });
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
if (Array.isArray(data)) return data;
|
|
}
|
|
} catch (e) {}
|
|
|
|
return [
|
|
{
|
|
text: "Footnotes aren't decorations. They're load-bearing walls.",
|
|
source: "IF.TTT paper",
|
|
href: "https://infrafabric.io/static/hosted/review/ifttt-paper-update/2025-12-28/review-pack.html",
|
|
},
|
|
{
|
|
text: "If there's no IF.TTT trace, it didn't happen—or shouldn't be trusted.",
|
|
source: "IF.TTT doctrine",
|
|
href: "https://infrafabric.io/static/hosted/review/ifttt-paper-update/2025-12-28/review-pack.html",
|
|
},
|
|
{
|
|
text: "Trust isn't claimed. It's proven.",
|
|
source: "IF.TTT paper",
|
|
href: "https://infrafabric.io/static/hosted/review/ifttt-paper-update/2025-12-28/review-pack.html",
|
|
},
|
|
];
|
|
}
|
|
|
|
function resolveIfTttUrl(path) {
|
|
try {
|
|
const scriptEl = document.querySelector('script[src$="app.js"]');
|
|
const scriptUrl = scriptEl ? new URL(scriptEl.getAttribute("src"), window.location.href) : new URL(window.location.href);
|
|
return new URL(path, scriptUrl).toString();
|
|
} catch (e) {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
function renderQuoteMeta({ quoteMetaEl, q }) {
|
|
while (quoteMetaEl.firstChild) quoteMetaEl.removeChild(quoteMetaEl.firstChild);
|
|
|
|
const source = String(q.source || "").trim();
|
|
const href = String(q.href || "").trim();
|
|
if (!source) return;
|
|
|
|
if (href) {
|
|
const a = document.createElement("a");
|
|
a.href = href;
|
|
a.target = "_blank";
|
|
a.rel = "noreferrer";
|
|
a.textContent = source;
|
|
quoteMetaEl.appendChild(a);
|
|
return;
|
|
}
|
|
|
|
quoteMetaEl.textContent = source;
|
|
}
|
|
|
|
function estimateReadMs(text) {
|
|
const cleaned = String(text || "").trim();
|
|
if (!cleaned) return 4000;
|
|
|
|
const words = cleaned.split(/\s+/).filter(Boolean).length;
|
|
const wpm = 220;
|
|
const ms = (words / wpm) * 60000 + 1200;
|
|
return clamp(ms, 3200, 11000);
|
|
}
|
|
|
|
function clamp(v, min, max) {
|
|
return Math.max(min, Math.min(max, v));
|
|
}
|