hosted/ifttt/app.js
2025-12-29 09:05:00 +00:00

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));
}