368 lines
16 KiB
JavaScript
368 lines
16 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const KEY = "zxh_hidden_details_v1";
|
|
const now = new Date();
|
|
const hour = now.getHours();
|
|
const path = window.location.pathname;
|
|
const state = readState();
|
|
|
|
const details = [
|
|
"The footer keeps a tiny chair for anyone who scrolls this far.",
|
|
"Someone labelled the spare cables: important, mysterious, probably for a printer.",
|
|
"Memory is just a room that learned how to echo kindly.",
|
|
"If you are reading this at night, please unclench your jaw.",
|
|
"A bookmark fell asleep here and dreamed it was a door.",
|
|
"SYSTEM: kettle confidence restored.",
|
|
"Mum says the rice is never late, only thinking.",
|
|
"Dad left a note: check the plug, then check your assumptions.",
|
|
"The attic server hums like it knows an old song.",
|
|
"A small lantern named Morrow was seen under the navigation bar.",
|
|
"Home is the place where the mismatched mugs know your hands.",
|
|
"This tooltip has been waiting all afternoon to feel useful.",
|
|
"404s are just hallways with very sincere lighting.",
|
|
"The website looked away politely while you reread that sentence.",
|
|
"Winter folded the garden into a quiet letter.",
|
|
"Spring arrived wearing yesterday's coat and nobody minded.",
|
|
"Autumn keeps receipts from every golden hour.",
|
|
"Summer is a glass of water forgotten beside a book.",
|
|
"Auntie wrote: do not trust empty biscuit tins. They are emotionally complicated.",
|
|
"The comment box occasionally practices being brave.",
|
|
"There is soup in the archive, but only metaphorically. Probably.",
|
|
"Every old hard drive contains at least one photo called final_final_really.",
|
|
"The cursor paused here once. We still talk about it.",
|
|
"Friendship is two people saving the better chair without discussing it.",
|
|
"Tonight, the page is trying to be a porch light.",
|
|
"A tiny bell rings whenever someone chooses kindness and nobody notices.",
|
|
"The moon asked the stylesheet for softer margins.",
|
|
"You have visited before. The dust remembered your shoes.",
|
|
"The search box knows more than it says and less than it implies.",
|
|
"A good recipe is a family argument that became edible.",
|
|
"Grandad marked this line with a pencil and then forgot why.",
|
|
"Do not feed the calendar after midnight; it gets sentimental.",
|
|
"The page is handmade. Some fingerprints are load-bearing.",
|
|
"Some dreams are just old rooms asking to be opened gently.",
|
|
"Loneliness is not proof that you are hard to love.",
|
|
"The sidebar once tried poetry. It was mostly indentation.",
|
|
"If the site blinks, blink back. It is only being polite.",
|
|
"The archive has a cousin called Nettle who files everything under Maybe.",
|
|
"Morrow says: the lake is not empty, it is saving room.",
|
|
"A spoon knocked twice from inside the recipe page.",
|
|
"There are no achievements here. Only small doors and warm lights.",
|
|
"The browser tab is pretending to be calm.",
|
|
"This sentence was left in a coat pocket from 2009.",
|
|
"A family tradition: someone says five minutes and everyone understands twenty.",
|
|
"The oldest photo on the shelf still remembers the smell of rain.",
|
|
"Some pages do not want to be found quickly.",
|
|
"The loading screen packed snacks, just in case.",
|
|
"You are allowed to leave a page unfinished.",
|
|
"The website is not judging your tabs. It has seen worse.",
|
|
"A small blue bowl appears in three memories and no inventories.",
|
|
"The old internet was slower, which gave anticipation somewhere to sit.",
|
|
"A guestbook entry from nobody says: I was here, kindly.",
|
|
"The clock does not chase you here.",
|
|
"Nettle borrowed the semicolon and promised to bring it back.",
|
|
"Every link is a tiny act of trust.",
|
|
"The error page brought a blanket.",
|
|
"Sometimes growing older is just learning which lights to leave on.",
|
|
"The hallway smells faintly of toast and forgotten passwords.",
|
|
"A dream page exists behind a word you have not clicked enough.",
|
|
"This site remembers visits, not identities.",
|
|
"A note under the doormat reads: take your time.",
|
|
"The system log says: heart rebooted successfully.",
|
|
"Somebody dusted the pixels before you arrived.",
|
|
"Morrow has a lantern. Nettle has a clipboard. Neither has a plan.",
|
|
"The kitchen light is on because somebody might come home late.",
|
|
"If you found this by accident, that still counts as being invited.",
|
|
"The page tried to hum but settled for rendering.",
|
|
"A childhood afternoon is stored here at room temperature.",
|
|
"The comments are quiet because they are listening.",
|
|
"The sitemap has dreams of becoming a treasure map.",
|
|
"Do not worry if the day felt unfinished. Most good days do.",
|
|
"This button has been clicked before and took it personally.",
|
|
"A cupboard in the footer contains emergency biscuits and one tiny apology.",
|
|
"Nostalgia is a window with fingerprints on both sides.",
|
|
"A cousin once renamed the Wi-Fi to Pretty Please Work.",
|
|
"The site keeps a spare sunrise for difficult mornings.",
|
|
"You can rest your eyes. The paragraph will wait.",
|
|
"A missing page is still a page if it misses you back.",
|
|
"The kettle, the clock, and the router have formed a committee.",
|
|
"Sometimes the kindest archive is the one that lets things fade.",
|
|
"The moon cached this page for offline tenderness.",
|
|
"A small paper star is stuck behind the header.",
|
|
"The old forum signature reads: be right back, becoming a person.",
|
|
"SYSTEM: nostalgia level acceptable. Tea recommended.",
|
|
"A dream wrote itself in lowercase and hid under the bed.",
|
|
"There is a room in the site where every chair faces the window.",
|
|
"A soup recipe was annotated with: add patience until it smells like home.",
|
|
"The site has a memory of dial-up and uses it only for dramatic timing.",
|
|
"Kindness is often just good timing with shoes on.",
|
|
"Some nights the archive counts sheep in binary.",
|
|
"The lamp on the study page knows exactly when you are tired.",
|
|
"A little paper boat named Afterward is still crossing the margin.",
|
|
"The scroll bar has travelled far and asks for no medal.",
|
|
"A fake system warning: feelings detected in production.",
|
|
"This hidden message has no puzzle value. It is just glad you came.",
|
|
"If today was heavy, put it down beside the footer for a minute.",
|
|
"Morrow saw your cursor and pretended not to.",
|
|
"A family note says: whoever finishes the milk becomes the historian.",
|
|
"The website keeps one chair empty for the person you used to be.",
|
|
"There is a tiny weather report inside the logo: possible comfort.",
|
|
"The secret is not that the site is alive. The secret is that you are.",
|
|
"A final scrap says: come back when the page is older."
|
|
];
|
|
|
|
const poems = [
|
|
"A room remembers softly / the coat hook, the rain, the key / and you, arriving later.",
|
|
"Time put sugar in its tea / and called the afternoon / almost enough.",
|
|
"An old friend writes once / and the whole year / grows a window.",
|
|
"Dreams leave socks on the floor / and deny everything / by morning.",
|
|
"Home is not a place / so much as a light / that forgives your lateness.",
|
|
"The garden went quiet / not from sadness / but from listening to roots.",
|
|
"A childhood summer / fits in one glass marble / if you hold it up to sun.",
|
|
"The clock kept walking / so I gave it a chair / and sat beside it."
|
|
];
|
|
|
|
const greetings = [
|
|
"The page saved you a seat.",
|
|
"Welcome back to the handmade part of the internet.",
|
|
"The kettle just clicked. Good timing.",
|
|
"A small light is on in the archive.",
|
|
"Today has entered quietly.",
|
|
"You are visitor number: emotionally significant.",
|
|
"The dashboard shuffled its papers before you arrived.",
|
|
"Morrow says hello from somewhere under the floorboards."
|
|
];
|
|
|
|
const nightMessages = [
|
|
"It is late here. Drink some water before the next page.",
|
|
"Night mode inside night mode: you are allowed to be gentle with yourself.",
|
|
"The site lowered its voice because the house is sleeping.",
|
|
"A late visitor gets the soft lamp."
|
|
];
|
|
|
|
function readState() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(KEY) || "{}");
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function writeState(next) {
|
|
try {
|
|
localStorage.setItem(KEY, JSON.stringify(next));
|
|
} catch (_) {}
|
|
}
|
|
|
|
function pick(list, salt) {
|
|
const seed = hash(`${path}|${now.toDateString()}|${salt || ""}`);
|
|
return list[seed % list.length];
|
|
}
|
|
|
|
function hash(value) {
|
|
let h = 2166136261;
|
|
for (let i = 0; i < value.length; i += 1) {
|
|
h ^= value.charCodeAt(i);
|
|
h = Math.imul(h, 16777619);
|
|
}
|
|
return Math.abs(h >>> 0);
|
|
}
|
|
|
|
function toast(message, duration) {
|
|
let el = document.querySelector(".hidden-toast");
|
|
if (!el) {
|
|
el = document.createElement("div");
|
|
el.className = "hidden-toast";
|
|
el.setAttribute("role", "status");
|
|
document.body.appendChild(el);
|
|
}
|
|
el.textContent = message;
|
|
el.classList.add("is-visible");
|
|
window.clearTimeout(el._timer);
|
|
el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5200);
|
|
}
|
|
|
|
function rememberVisit() {
|
|
const visits = (state.visits || 0) + 1;
|
|
const seen = Array.isArray(state.seen) ? state.seen : [];
|
|
const next = {
|
|
...state,
|
|
visits,
|
|
lastPath: path,
|
|
seen: Array.from(new Set([...seen, path])).slice(-80),
|
|
firstSeen: state.firstSeen || new Date().toISOString(),
|
|
lastSeen: new Date().toISOString()
|
|
};
|
|
writeState(next);
|
|
return next;
|
|
}
|
|
|
|
function addFooterNote(memory) {
|
|
const footer = document.querySelector("footer");
|
|
if (!footer) return;
|
|
const note = document.createElement("p");
|
|
note.className = "hidden-footer-note";
|
|
const base = pick(details, "footer");
|
|
note.textContent = memory.visits > 4 ? `${base} You have passed through ${memory.visits} times.` : base;
|
|
footer.appendChild(note);
|
|
}
|
|
|
|
function addHomepageGreeting(memory) {
|
|
const target = document.querySelector("#db-greeting");
|
|
if (!target) return;
|
|
const greeting = pick(greetings, `greeting-${memory.visits}`);
|
|
target.textContent = greeting;
|
|
const extra = document.createElement("p");
|
|
extra.className = "hidden-greeting";
|
|
extra.textContent = memory.visits > 1 ? "The site remembers this is not your first doorway." : "First visits count as tiny ceremonies.";
|
|
target.closest("div")?.appendChild(extra);
|
|
}
|
|
|
|
function addWhispers() {
|
|
const paragraphs = Array.from(document.querySelectorAll("main p, #content p, article p")).filter((p) => p.textContent.trim().length > 80);
|
|
paragraphs.slice(0, 4).forEach((p, index) => {
|
|
if ((hash(path + index) + index) % 3 !== 0) return;
|
|
const mark = document.createElement("span");
|
|
mark.className = "hidden-whisper";
|
|
mark.tabIndex = 0;
|
|
mark.textContent = " ?";
|
|
mark.title = pick(index % 2 ? poems : details, `whisper-${index}`);
|
|
mark.addEventListener("click", () => toast(mark.title));
|
|
p.appendChild(mark);
|
|
});
|
|
}
|
|
|
|
function addCornerObject(memory) {
|
|
const object = document.createElement("button");
|
|
object.className = "hidden-corner-object";
|
|
object.type = "button";
|
|
object.title = "A very small archive object";
|
|
object.textContent = memory.visits % 2 ? "*" : "~";
|
|
document.body.appendChild(object);
|
|
|
|
let clicks = 0;
|
|
object.addEventListener("click", () => {
|
|
clicks += 1;
|
|
const messages = [
|
|
"The object is warmer than before.",
|
|
"Tap tap. Somewhere, a spoon answers.",
|
|
"Morrow whispers: that is not a button, but it appreciates the attention.",
|
|
"Nettle files a report titled Repeated Kindness.",
|
|
"A loose panel opens in the idea of the page."
|
|
];
|
|
toast(messages[Math.min(clicks - 1, messages.length - 1)]);
|
|
if (clicks === 7) {
|
|
window.location.href = "/play/left-behind.html";
|
|
}
|
|
});
|
|
}
|
|
|
|
function addPatterns(memory) {
|
|
const nav = document.querySelector(".site-brand");
|
|
if (nav) {
|
|
let taps = Number(sessionStorage.getItem("zxh_logo_taps") || 0);
|
|
nav.addEventListener("click", (event) => {
|
|
if (path === "/" || path === "/index.html") {
|
|
event.preventDefault();
|
|
taps += 1;
|
|
sessionStorage.setItem("zxh_logo_taps", String(taps));
|
|
toast(taps >= 5 ? "The logo opens one eye. Try /play/dream-corridor.html" : "The logo makes a tiny ceramic sound.");
|
|
if (taps >= 8) window.location.href = "/play/dream-corridor.html";
|
|
}
|
|
});
|
|
}
|
|
|
|
const search = document.querySelector("#search-input");
|
|
if (search) {
|
|
search.addEventListener("input", () => {
|
|
const value = search.value.trim().toLowerCase();
|
|
if (value === "morrow") toast("Search result: lantern, lake, unfinished apology.");
|
|
if (value === "nettle") toast("Search result: clipboard, cupboard, suspicious semicolon.");
|
|
if (value === "home") toast("Search result: warm window, spare key, somebody humming downstairs.");
|
|
if (value === "kitchen light") window.location.href = "/play/kitchen-light.html";
|
|
});
|
|
}
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
const key = event.key.toLowerCase();
|
|
const chain = `${sessionStorage.getItem("zxh_key_chain") || ""}${key}`.slice(-12);
|
|
sessionStorage.setItem("zxh_key_chain", chain);
|
|
if (chain.endsWith("remember")) toast("The website remembers gently, which is the only useful kind.");
|
|
if (chain.endsWith("dream")) window.location.href = "/play/dream-corridor.html";
|
|
});
|
|
|
|
if (memory.seen?.length >= 6 && !state.travellerNoteShown) {
|
|
window.setTimeout(() => {
|
|
toast("The map noticed you wandering and left the hallway lights on.");
|
|
writeState({ ...readState(), travellerNoteShown: true });
|
|
}, 1600);
|
|
}
|
|
}
|
|
|
|
function addRareEvents() {
|
|
if (hour >= 22 || hour < 5) {
|
|
document.body.classList.add("hidden-late-night");
|
|
window.setTimeout(() => toast(pick(nightMessages, "night"), 2200), 900);
|
|
}
|
|
|
|
const roll = Math.random();
|
|
if (roll < 0.004) {
|
|
window.setTimeout(() => showDriftNote("A very rare page breeze passed through.", " .--.\n ( ) <- a cloud pretending to be a thought\n `--'"), 1800);
|
|
} else if (roll < 0.012) {
|
|
window.setTimeout(() => toast("RARE SYSTEM EVENT: the archive found a biscuit in its coat pocket."), 2400);
|
|
}
|
|
}
|
|
|
|
function showDriftNote(text, art) {
|
|
const panel = document.createElement("aside");
|
|
panel.className = "hidden-drift-note";
|
|
panel.innerHTML = `<div>${escapeHtml(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
|
|
panel.querySelector("button").addEventListener("click", () => panel.remove());
|
|
document.body.appendChild(panel);
|
|
}
|
|
|
|
function addSecretLinks() {
|
|
[
|
|
["/play/left-behind.html", "left behind"],
|
|
["/play/dream-corridor.html", "dream corridor"],
|
|
["/play/kitchen-light.html", "kitchen light"]
|
|
].forEach(([href, label], index) => {
|
|
const link = document.createElement("a");
|
|
link.className = "hidden-secret-link";
|
|
link.href = href;
|
|
link.textContent = label;
|
|
link.style.left = `${index + 1}px`;
|
|
document.body.appendChild(link);
|
|
});
|
|
}
|
|
|
|
function enhanceErrors() {
|
|
if (document.title.match(/404|not found/i) || document.body.textContent.match(/404|not found/i)) {
|
|
toast("This page is missing, but you are not. Try the sitemap, or sit here a second.");
|
|
}
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value).replace(/[&<>"']/g, (char) => ({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'"
|
|
}[char]));
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const memory = rememberVisit();
|
|
addFooterNote(memory);
|
|
addHomepageGreeting(memory);
|
|
addWhispers();
|
|
addCornerObject(memory);
|
|
addPatterns(memory);
|
|
addRareEvents();
|
|
addSecretLinks();
|
|
enhanceErrors();
|
|
});
|
|
}());
|