254 lines
7.3 KiB
JavaScript
254 lines
7.3 KiB
JavaScript
(() => {
|
||
const howStepEl = document.getElementById("howStep");
|
||
const howDetailEl = document.getElementById("howDetail");
|
||
if (howStepEl && howDetailEl) startHowItWorks(howStepEl, howDetailEl);
|
||
|
||
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 startHowItWorks(stepEl, detailEl) {
|
||
const prefersReducedMotion =
|
||
typeof window !== "undefined" &&
|
||
window.matchMedia &&
|
||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||
|
||
const steps = [
|
||
{
|
||
step: "1) You write the confidential document.",
|
||
detail: "Keep the source private. Don’t publish it to “prove” it exists.",
|
||
},
|
||
{
|
||
step: "2) Your system produces an output.",
|
||
detail: "A summary, decision, report, message, or answer.",
|
||
},
|
||
{
|
||
step: "3) IF.Trace binds source → output.",
|
||
detail: "Hashes + trace id + proof links (so evidence can be checked later).",
|
||
},
|
||
{
|
||
step: "4) You share proof, not your raw data.",
|
||
detail: "Third parties verify without needing your accounts or logins.",
|
||
},
|
||
{
|
||
step: "5) If there’s no trace, it’s not trustworthy.",
|
||
detail: "A simple rule that survives vendors, contractors, and audits.",
|
||
},
|
||
];
|
||
|
||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
||
const typeInto = async (el, text, opts = {}) => {
|
||
const minDelayMs = opts.minDelayMs ?? 12;
|
||
const maxDelayMs = opts.maxDelayMs ?? 26;
|
||
|
||
el.textContent = "";
|
||
for (let idx = 0; idx < text.length; idx += 1) {
|
||
el.textContent += text[idx];
|
||
const jitter = Math.floor(Math.random() * (maxDelayMs - minDelayMs + 1));
|
||
await sleep(minDelayMs + jitter);
|
||
}
|
||
};
|
||
|
||
const run = async () => {
|
||
if (prefersReducedMotion) {
|
||
const first = steps[0];
|
||
stepEl.textContent = first.step;
|
||
detailEl.textContent = first.detail;
|
||
return;
|
||
}
|
||
|
||
let idx = 0;
|
||
// eslint-disable-next-line no-constant-condition
|
||
while (true) {
|
||
const current = steps[idx % steps.length];
|
||
await typeInto(stepEl, current.step);
|
||
await sleep(120);
|
||
await typeInto(detailEl, current.detail, { minDelayMs: 8, maxDelayMs: 18 });
|
||
await sleep(2200);
|
||
|
||
stepEl.textContent = "";
|
||
detailEl.textContent = "";
|
||
await sleep(240);
|
||
|
||
idx += 1;
|
||
}
|
||
};
|
||
|
||
void run();
|
||
}
|
||
|
||
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.Trace paper",
|
||
href: "https://infrafabric.io/static/hosted/review/ifttt-paper-update/2025-12-28/review-pack.html",
|
||
},
|
||
{
|
||
text: "If there's no IF.Trace trace, it didn't happen—or shouldn't be trusted.",
|
||
source: "IF.Trace 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.Trace 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));
|
||
}
|