hosted/ifttt/app.js
2025-12-30 04:54:17 +00:00

254 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
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. Dont 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 theres no trace, its 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));
}