This commit is contained in:
@@ -1,13 +1,37 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const KEY = "zxh_hidden_details_v2";
|
/*
|
||||||
const OLD_KEY = "zxh_hidden_details_v1";
|
* Hidden site details
|
||||||
|
* -------------------
|
||||||
|
* This file adds small hidden interactions across the site: footer notes,
|
||||||
|
* click targets, search/keyboard secrets, rare messages, and page-specific
|
||||||
|
* behavior for the hidden /play pages.
|
||||||
|
*
|
||||||
|
* How to add your own things:
|
||||||
|
* - Add new text to EDITABLE CONTENT arrays such as `details`, `poems`,
|
||||||
|
* `greetings`, or `lore`.
|
||||||
|
* - Add search-triggered toast messages to `SEARCH_TOASTS`.
|
||||||
|
* - Add search-triggered page redirects to `SEARCH_ROUTES`.
|
||||||
|
* - Add typed keyboard phrases to `KEYBOARD_SECRETS` or `LONG_KEYBOARD_SECRETS`.
|
||||||
|
* - Add a new feature by writing an `addYourFeature(memory)` function and
|
||||||
|
* adding it to the `FEATURES` list at the bottom of the file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Storage keys. v1 is read once so old visitors keep their hidden progress.
|
||||||
|
const STORAGE_KEY = "zxh_hidden_details_v2";
|
||||||
|
const OLD_STORAGE_KEY = "zxh_hidden_details_v1";
|
||||||
|
|
||||||
|
// Shared page context. These are captured once so daily random picks stay stable.
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const hour = now.getHours();
|
const hour = now.getHours();
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
const state = readState();
|
const state = readState();
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// EDITABLE CONTENT
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
const familyLayers = [
|
const familyLayers = [
|
||||||
"Layer 0: surface website - notes, pages, tools, normal navigation.",
|
"Layer 0: surface website - notes, pages, tools, normal navigation.",
|
||||||
"Layer 1: casual hidden jokes - Aphy notices clicks, typos, tabs, and old-web habits.",
|
"Layer 1: casual hidden jokes - Aphy notices clicks, typos, tabs, and old-web habits.",
|
||||||
@@ -244,16 +268,69 @@
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Exact search text -> toast message.
|
||||||
|
const SEARCH_TOASTS = {
|
||||||
|
lima: "Search result: warmth, Oct 2025, soon-to-be home.",
|
||||||
|
aphy: "Aphy: present. Mostly helpful. Occasionally poetic by accident.",
|
||||||
|
"sensei chi": "Sensei Chi: the search is also searching you.",
|
||||||
|
"young z": "Search result: age 7, blanket cape, crayon sun.",
|
||||||
|
"future z": "Future Z: I cannot spoil the ending. I can say keep going.",
|
||||||
|
"2022": "lima layer: beginning stored as warmth, not trivia.",
|
||||||
|
"oct 2025": "Engagement memory: the calendar learned to glow.",
|
||||||
|
"age 7": "Young Z layer: crayons, cartoons, and a cape made from a blanket.",
|
||||||
|
"age 40": "Future Z: I am tired sometimes, but not defeated."
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exact search text -> hidden page route.
|
||||||
|
const SEARCH_ROUTES = {
|
||||||
|
"kitchen light": "/play/kitchen-light.html",
|
||||||
|
"lima note": "/play/lima-note.html",
|
||||||
|
"aphy console": "/play/aphy-console.html",
|
||||||
|
"sensei garden": "/play/sensei-garden.html",
|
||||||
|
"young z desk": "/play/young-z-desk.html",
|
||||||
|
"future log": "/play/future-log.html",
|
||||||
|
"family layer index": "/play/family-layer-index.html",
|
||||||
|
"do not open": "/play/do-not-open.html",
|
||||||
|
"voice note": "/play/cassette-log.html",
|
||||||
|
"time platform": "/play/train-platform.html",
|
||||||
|
"unfinished letter": "/play/forgotten-draft.html",
|
||||||
|
"burnt sugar": "/play/corrupted-recipe.html",
|
||||||
|
cupboard: "/play/terminal-cupboard.html",
|
||||||
|
patience: "/play/patience-game.html"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Short typed phrases. Stored in sessionStorage as a rolling key chain.
|
||||||
|
const KEYBOARD_SECRETS = [
|
||||||
|
{ phrase: "remember", message: "lima keeps the memory grounded. Aphy keeps the backup." },
|
||||||
|
{ phrase: "dream", route: "/play/dream-corridor.html" },
|
||||||
|
{ phrase: "layer index", route: "/play/family-layer-index.html" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Longer typed phrases and character names.
|
||||||
|
const LONG_KEYBOARD_SECRETS = [
|
||||||
|
{ phrase: "leave the light on", route: "/play/kitchen-light.html" },
|
||||||
|
{ phrase: "oldweb", message: "Aphy briefly considers a table layout, then apologizes to CSS." },
|
||||||
|
{ phrase: "lima", message: "lima note: I am glad you made something this sincere." },
|
||||||
|
{ phrase: "sensei", message: "Sensei Chi: a hidden word is still a word." },
|
||||||
|
{ phrase: "young z", message: "Young Z has drawn a door in the margin." },
|
||||||
|
{ phrase: "future z", message: "Future Z: keep the gentle parts. They compound." },
|
||||||
|
{ phrase: "aphy", song: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// STATE HELPERS
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
function readState() {
|
function readState() {
|
||||||
let current = {};
|
let current = {};
|
||||||
try {
|
try {
|
||||||
current = JSON.parse(localStorage.getItem(KEY) || "{}");
|
current = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
if (!current.visits) {
|
if (!current.visits) {
|
||||||
try {
|
try {
|
||||||
const oldState = JSON.parse(localStorage.getItem(OLD_KEY) || "{}");
|
const oldState = JSON.parse(localStorage.getItem(OLD_STORAGE_KEY) || "{}");
|
||||||
current = { ...oldState, migratedFromV1: true };
|
current = { ...oldState, migratedFromV1: true };
|
||||||
localStorage.setItem(KEY, JSON.stringify(current));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
return current;
|
return current;
|
||||||
@@ -261,7 +338,7 @@
|
|||||||
|
|
||||||
function writeState(next) {
|
function writeState(next) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(KEY, JSON.stringify(next));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,6 +356,10 @@
|
|||||||
return Math.abs(h >>> 0);
|
return Math.abs(h >>> 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// UI HELPERS
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
function toast(message, duration) {
|
function toast(message, duration) {
|
||||||
let el = document.querySelector(".hidden-toast");
|
let el = document.querySelector(".hidden-toast");
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -293,6 +374,22 @@
|
|||||||
el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600);
|
el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function redirectTo(route) {
|
||||||
|
window.location.href = route;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runSecretAction(secret) {
|
||||||
|
if (secret.route) redirectTo(secret.route);
|
||||||
|
if (secret.message) toast(secret.message);
|
||||||
|
if (secret.song) playTinySong();
|
||||||
|
}
|
||||||
|
|
||||||
|
function layerForSearch(value) {
|
||||||
|
if (value.includes("future")) return 4;
|
||||||
|
if (value.includes("sensei") || value.includes("aphy")) return 3;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
function rememberVisit() {
|
function rememberVisit() {
|
||||||
const visits = (state.visits || 0) + 1;
|
const visits = (state.visits || 0) + 1;
|
||||||
const seen = Array.isArray(state.seen) ? state.seen : [];
|
const seen = Array.isArray(state.seen) ? state.seen : [];
|
||||||
@@ -321,6 +418,10 @@
|
|||||||
writeState(next);
|
writeState(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// GLOBAL FEATURES
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
function addFooterNote(memory) {
|
function addFooterNote(memory) {
|
||||||
const footer = document.querySelector("footer");
|
const footer = document.querySelector("footer");
|
||||||
if (!footer) return;
|
if (!footer) return;
|
||||||
@@ -342,6 +443,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addWhispers() {
|
function addWhispers() {
|
||||||
|
// Event: click one of the tiny "?" marks appended to long paragraphs.
|
||||||
const paragraphs = Array.from(document.querySelectorAll("main p, #content p, article p")).filter((p) => p.textContent.trim().length > 80);
|
const paragraphs = Array.from(document.querySelectorAll("main p, #content p, article p")).filter((p) => p.textContent.trim().length > 80);
|
||||||
paragraphs.slice(0, 5).forEach((p, index) => {
|
paragraphs.slice(0, 5).forEach((p, index) => {
|
||||||
if ((hash(path + index) + index) % 3 !== 0) return;
|
if ((hash(path + index) + index) % 3 !== 0) return;
|
||||||
@@ -359,6 +461,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addCornerObject() {
|
function addCornerObject() {
|
||||||
|
// Event: click the small fixed button in the bottom-left corner.
|
||||||
const object = document.createElement("button");
|
const object = document.createElement("button");
|
||||||
object.className = "hidden-corner-object";
|
object.className = "hidden-corner-object";
|
||||||
object.type = "button";
|
object.type = "button";
|
||||||
@@ -381,7 +484,11 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPatterns(memory) {
|
function addLogoSearchAndKeyboardSecrets(memory) {
|
||||||
|
// Events:
|
||||||
|
// - Click `.site-brand` repeatedly on the homepage.
|
||||||
|
// - Type exact words into `#search-input`.
|
||||||
|
// - Type phrases anywhere on the page.
|
||||||
const nav = document.querySelector(".site-brand");
|
const nav = document.querySelector(".site-brand");
|
||||||
if (nav) {
|
if (nav) {
|
||||||
let taps = Number(sessionStorage.getItem("zxh_logo_taps") || 0);
|
let taps = Number(sessionStorage.getItem("zxh_logo_taps") || 0);
|
||||||
@@ -401,12 +508,11 @@
|
|||||||
if (search) {
|
if (search) {
|
||||||
search.addEventListener("input", () => {
|
search.addEventListener("input", () => {
|
||||||
const value = search.value.trim().toLowerCase();
|
const value = search.value.trim().toLowerCase();
|
||||||
if (value === "lima") toast("Search result: warmth, Oct 2025, soon-to-be home.");
|
if (SEARCH_TOASTS[value]) toast(SEARCH_TOASTS[value]);
|
||||||
if (value === "aphy") toast("Aphy: present. Mostly helpful. Occasionally poetic by accident.");
|
if (SEARCH_ROUTES[value]) {
|
||||||
if (value === "sensei chi") toast("Sensei Chi: the search is also searching you.");
|
awardLayer(layerForSearch(value), `search ${value}`);
|
||||||
if (value === "young z") toast("Search result: age 7, blanket cape, crayon sun.");
|
redirectTo(SEARCH_ROUTES[value]);
|
||||||
if (value === "future z") toast("Future Z: I cannot spoil the ending. I can say keep going.");
|
}
|
||||||
if (value === "kitchen light") window.location.href = "/play/kitchen-light.html";
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,9 +520,9 @@
|
|||||||
const key = event.key.toLowerCase();
|
const key = event.key.toLowerCase();
|
||||||
const chain = `${sessionStorage.getItem("zxh_key_chain") || ""}${key}`.slice(-18);
|
const chain = `${sessionStorage.getItem("zxh_key_chain") || ""}${key}`.slice(-18);
|
||||||
sessionStorage.setItem("zxh_key_chain", chain);
|
sessionStorage.setItem("zxh_key_chain", chain);
|
||||||
if (chain.endsWith("remember")) toast("lima keeps the memory grounded. Aphy keeps the backup.");
|
KEYBOARD_SECRETS
|
||||||
if (chain.endsWith("dream")) window.location.href = "/play/dream-corridor.html";
|
.filter((secret) => chain.endsWith(secret.phrase))
|
||||||
if (chain.endsWith("layer index")) window.location.href = "/play/family-layer-index.html";
|
.forEach(runSecretAction);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (memory.seen?.length >= 6 && !state.travellerNoteShown) {
|
if (memory.seen?.length >= 6 && !state.travellerNoteShown) {
|
||||||
@@ -519,6 +625,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addObjectConstellation() {
|
function addObjectConstellation() {
|
||||||
|
// Event: click the small keepsake buttons along the bottom rail.
|
||||||
const rail = document.createElement("div");
|
const rail = document.createElement("div");
|
||||||
rail.className = "hidden-object-rail";
|
rail.className = "hidden-object-rail";
|
||||||
rail.setAttribute("aria-label", "Family Layer Index objects");
|
rail.setAttribute("aria-label", "Family Layer Index objects");
|
||||||
@@ -551,6 +658,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addWeatherWindow(memory) {
|
function addWeatherWindow(memory) {
|
||||||
|
// Event: click the small "window" button. This asks for browser geolocation.
|
||||||
if (memory.visits < 3 || !("geolocation" in navigator)) return;
|
if (memory.visits < 3 || !("geolocation" in navigator)) return;
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.className = "hidden-weather-window";
|
button.className = "hidden-weather-window";
|
||||||
@@ -610,51 +718,19 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSearchAndKeySecrets() {
|
function addLongKeyboardSecrets() {
|
||||||
const search = document.querySelector("#search-input");
|
// Event: type longer hidden phrases anywhere on the page.
|
||||||
if (search) {
|
|
||||||
search.addEventListener("input", () => {
|
|
||||||
const value = search.value.trim().toLowerCase();
|
|
||||||
const routes = {
|
|
||||||
"lima note": "/play/lima-note.html",
|
|
||||||
"aphy console": "/play/aphy-console.html",
|
|
||||||
"sensei garden": "/play/sensei-garden.html",
|
|
||||||
"young z desk": "/play/young-z-desk.html",
|
|
||||||
"future log": "/play/future-log.html",
|
|
||||||
"family layer index": "/play/family-layer-index.html",
|
|
||||||
"do not open": "/play/do-not-open.html",
|
|
||||||
"voice note": "/play/cassette-log.html",
|
|
||||||
"time platform": "/play/train-platform.html",
|
|
||||||
"unfinished letter": "/play/forgotten-draft.html",
|
|
||||||
"burnt sugar": "/play/corrupted-recipe.html",
|
|
||||||
"cupboard": "/play/terminal-cupboard.html",
|
|
||||||
"patience": "/play/patience-game.html"
|
|
||||||
};
|
|
||||||
if (routes[value]) {
|
|
||||||
awardLayer(value.includes("future") ? 4 : value.includes("sensei") || value.includes("aphy") ? 3 : 2, `search ${value}`);
|
|
||||||
window.location.href = routes[value];
|
|
||||||
}
|
|
||||||
if (value === "2022") toast("lima layer: beginning stored as warmth, not trivia.");
|
|
||||||
if (value === "oct 2025") toast("Engagement memory: the calendar learned to glow.");
|
|
||||||
if (value === "age 7") toast("Young Z layer: crayons, cartoons, and a cape made from a blanket.");
|
|
||||||
if (value === "age 40") toast("Future Z: I am tired sometimes, but not defeated.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
const chain = `${sessionStorage.getItem("zxh_second_chain") || ""}${event.key.toLowerCase()}`.slice(-24);
|
const chain = `${sessionStorage.getItem("zxh_second_chain") || ""}${event.key.toLowerCase()}`.slice(-24);
|
||||||
sessionStorage.setItem("zxh_second_chain", chain);
|
sessionStorage.setItem("zxh_second_chain", chain);
|
||||||
if (chain.endsWith("leave the light on")) window.location.href = "/play/kitchen-light.html";
|
LONG_KEYBOARD_SECRETS
|
||||||
if (chain.endsWith("oldweb")) toast("Aphy briefly considers a table layout, then apologizes to CSS.");
|
.filter((secret) => chain.endsWith(secret.phrase))
|
||||||
if (chain.endsWith("lima")) toast("lima note: I am glad you made something this sincere.");
|
.forEach(runSecretAction);
|
||||||
if (chain.endsWith("sensei")) toast("Sensei Chi: a hidden word is still a word.");
|
|
||||||
if (chain.endsWith("young z")) toast("Young Z has drawn a door in the margin.");
|
|
||||||
if (chain.endsWith("future z")) toast("Future Z: keep the gentle parts. They compound.");
|
|
||||||
if (chain.endsWith("aphy")) playTinySong();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPatienceRewards(memory) {
|
function addPatienceRewards(memory) {
|
||||||
|
// Event: no movement, key presses, scrolling, or clicking for 90 seconds.
|
||||||
if (memory.visits < 2) return;
|
if (memory.visits < 2) return;
|
||||||
let idleTimer = null;
|
let idleTimer = null;
|
||||||
const startIdle = () => {
|
const startIdle = () => {
|
||||||
@@ -701,6 +777,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addPageSpecificSecrets() {
|
function addPageSpecificSecrets() {
|
||||||
|
// Events only used by specific `/play/...` pages.
|
||||||
if (path.includes("/play/cassette-log")) {
|
if (path.includes("/play/cassette-log")) {
|
||||||
document.querySelectorAll("[data-cassette]").forEach((button, index) => {
|
document.querySelectorAll("[data-cassette]").forEach((button, index) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
@@ -789,27 +866,33 @@
|
|||||||
}[char]));
|
}[char]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Features run in this order on every page after the DOM is ready.
|
||||||
|
// Each function receives the current `memory` object.
|
||||||
|
const FEATURES = [
|
||||||
|
addFooterNote,
|
||||||
|
addHomepageGreeting,
|
||||||
|
addWhispers,
|
||||||
|
addCornerObject,
|
||||||
|
addLogoSearchAndKeyboardSecrets,
|
||||||
|
addRareEvents,
|
||||||
|
addSecretLinks,
|
||||||
|
enhanceErrors,
|
||||||
|
addContinuity,
|
||||||
|
addSeasonAndDates,
|
||||||
|
addObjectConstellation,
|
||||||
|
addWeatherWindow,
|
||||||
|
addOldWebLayer,
|
||||||
|
addSourceRelics,
|
||||||
|
addLongKeyboardSecrets,
|
||||||
|
addPatienceRewards,
|
||||||
|
addRareSecondLayer,
|
||||||
|
addFamilyLayerIndex,
|
||||||
|
addPageSpecificSecrets,
|
||||||
|
addInvisibleHoverSecrets
|
||||||
|
];
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const memory = rememberVisit();
|
const memory = rememberVisit();
|
||||||
addFooterNote(memory);
|
FEATURES.forEach((addFeature) => addFeature(memory));
|
||||||
addHomepageGreeting(memory);
|
|
||||||
addWhispers();
|
|
||||||
addCornerObject();
|
|
||||||
addPatterns(memory);
|
|
||||||
addRareEvents();
|
|
||||||
addSecretLinks();
|
|
||||||
enhanceErrors();
|
|
||||||
addContinuity(memory);
|
|
||||||
addSeasonAndDates(memory);
|
|
||||||
addObjectConstellation();
|
|
||||||
addWeatherWindow(memory);
|
|
||||||
addOldWebLayer(memory);
|
|
||||||
addSourceRelics();
|
|
||||||
addSearchAndKeySecrets();
|
|
||||||
addPatienceRewards(memory);
|
|
||||||
addRareSecondLayer(memory);
|
|
||||||
addFamilyLayerIndex(memory);
|
|
||||||
addPageSpecificSecrets(memory);
|
|
||||||
addInvisibleHoverSecrets();
|
|
||||||
});
|
});
|
||||||
}());
|
}());
|
||||||
|
|||||||
Reference in New Issue
Block a user