1333 lines
44 KiB
Org Mode
Executable File
1333 lines
44 KiB
Org Mode
Executable File
#+TITLE: The Rain Index
|
|
#+OPTIONS: num:nil title:nil toc:nil
|
|
#+NO_SIDENOTES: t
|
|
#+DATE: <2026-05-16 Sat>
|
|
|
|
#+BEGIN_EXPORT html
|
|
<style>
|
|
.content:has(.game),
|
|
#content.rain-index-content {
|
|
max-width: none !important;
|
|
width: 100% !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
}
|
|
|
|
body:has(.game),
|
|
body.rain-index-page {
|
|
margin: 0;
|
|
overflow-x: hidden;
|
|
background:
|
|
radial-gradient(circle at 8% 16%, rgba(163, 111, 44, 0.2), transparent 26rem),
|
|
radial-gradient(circle at 82% 12%, rgba(38, 56, 74, 0.22), transparent 24rem),
|
|
linear-gradient(125deg, #1e1a17, #473424 46%, #151c20);
|
|
}
|
|
|
|
body:has(.game) #postamble,
|
|
body.rain-index-page #postamble {
|
|
display: none;
|
|
}
|
|
|
|
.game {
|
|
--ink: #211b16;
|
|
--brown: #5f4631;
|
|
--paper: #e8ddc3;
|
|
--paper-2: #d7c6a7;
|
|
--paper-3: #f4ecd9;
|
|
--green: #243d35;
|
|
--blue: #26384a;
|
|
--red: #7f3330;
|
|
--gold: #a36f2c;
|
|
--glass: rgba(244, 236, 217, 0.74);
|
|
--shadow: rgba(28, 20, 13, 0.32);
|
|
--crt: rgba(143, 202, 164, 0.18);
|
|
color-scheme: light;
|
|
font-family: Georgia, "Times New Roman", serif;
|
|
position: relative;
|
|
width: 100%;
|
|
min-height: 680px;
|
|
height: 100dvh;
|
|
max-height: 100dvh;
|
|
overflow: hidden;
|
|
isolation: isolate;
|
|
cursor: none;
|
|
color: var(--ink);
|
|
background:
|
|
radial-gradient(circle at 8% 16%, rgba(163, 111, 44, 0.2), transparent 26rem),
|
|
radial-gradient(circle at 82% 12%, rgba(38, 56, 74, 0.22), transparent 24rem),
|
|
linear-gradient(125deg, #1e1a17, #473424 46%, #151c20);
|
|
}
|
|
|
|
.game *,
|
|
.game *::before,
|
|
.game *::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.game::before,
|
|
.game::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
z-index: 40;
|
|
}
|
|
|
|
.game::before {
|
|
opacity: 0.32;
|
|
background:
|
|
repeating-linear-gradient(0deg, rgba(255,255,255,0.04) 0 1px, transparent 1px 4px),
|
|
repeating-linear-gradient(90deg, rgba(25,17,11,0.06) 0 1px, transparent 1px 9px);
|
|
mix-blend-mode: soft-light;
|
|
}
|
|
|
|
.game::after {
|
|
opacity: 0.34;
|
|
background-image:
|
|
linear-gradient(90deg, transparent 0 12px, rgba(255,255,255,0.05) 13px, transparent 14px),
|
|
radial-gradient(circle at 22% 30%, rgba(255,255,255,0.09), transparent 0.15rem),
|
|
radial-gradient(circle at 76% 68%, rgba(0,0,0,0.1), transparent 0.18rem);
|
|
background-size: 17px 100%, 23px 31px, 29px 37px;
|
|
animation: rain 1.8s linear infinite;
|
|
}
|
|
|
|
.game button,
|
|
.game input {
|
|
font: inherit;
|
|
}
|
|
|
|
.game button {
|
|
color: inherit;
|
|
}
|
|
|
|
.desk {
|
|
position: absolute;
|
|
inset: clamp(10px, 2vw, 28px);
|
|
overflow: hidden;
|
|
border: 1px solid rgba(232, 221, 195, 0.34);
|
|
border-radius: 8px;
|
|
background:
|
|
linear-gradient(90deg, rgba(255,255,255,0.03), transparent 20% 72%, rgba(0,0,0,0.18)),
|
|
repeating-linear-gradient(6deg, rgba(244,236,217,0.045) 0 1px, transparent 1px 12px),
|
|
linear-gradient(145deg, rgba(232,221,195,0.16), rgba(20,17,14,0.18));
|
|
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.42), inset 0 0 90px rgba(0,0,0,0.28);
|
|
}
|
|
|
|
.zones {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
pointer-events: none;
|
|
opacity: 0.5;
|
|
transition: opacity 400ms ease;
|
|
}
|
|
|
|
.zone {
|
|
position: relative;
|
|
border: 1px dashed rgba(232, 221, 195, 0.14);
|
|
color: rgba(232, 221, 195, 0.32);
|
|
font: 700 0.68rem/1.1 "Courier New", monospace;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.zone span {
|
|
position: absolute;
|
|
left: 0.8rem;
|
|
top: 0.75rem;
|
|
}
|
|
|
|
.zone.is-lit {
|
|
background: rgba(232, 221, 195, 0.07);
|
|
color: rgba(245, 234, 201, 0.62);
|
|
}
|
|
|
|
.masthead {
|
|
position: absolute;
|
|
left: clamp(18px, 4vw, 54px);
|
|
top: clamp(16px, 4vw, 42px);
|
|
z-index: 12;
|
|
max-width: min(560px, calc(100vw - 2rem));
|
|
color: #efe4c9;
|
|
text-shadow: 0 2px 18px rgba(0,0,0,0.65);
|
|
user-select: none;
|
|
transition: transform 600ms ease, filter 600ms ease;
|
|
}
|
|
|
|
.masthead small {
|
|
display: block;
|
|
margin-bottom: 0.35rem;
|
|
color: rgba(236, 222, 186, 0.72);
|
|
font: 700 0.72rem/1.3 "Courier New", monospace;
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.masthead h1 {
|
|
margin: 0;
|
|
font-size: clamp(2.4rem, 10vw, 7.4rem);
|
|
font-weight: 500;
|
|
line-height: 0.83;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.masthead p {
|
|
width: min(39rem, 88vw);
|
|
margin: 1rem 0 0;
|
|
color: rgba(239, 228, 201, 0.76);
|
|
font-size: clamp(1rem, 1.8vw, 1.25rem);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.no-explain {
|
|
opacity: 0;
|
|
transform: translateY(5px);
|
|
transition: opacity 600ms ease, transform 600ms ease;
|
|
}
|
|
|
|
.game.has-touched .no-explain {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.machine {
|
|
position: absolute;
|
|
right: clamp(14px, 3vw, 42px);
|
|
top: clamp(14px, 3vw, 36px);
|
|
z-index: 20;
|
|
width: min(370px, calc(100vw - 2rem));
|
|
color: #d9f0cf;
|
|
font: 0.78rem/1.45 "Courier New", monospace;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.terminal {
|
|
min-height: 142px;
|
|
padding: 0.85rem;
|
|
border: 1px solid rgba(121, 205, 156, 0.28);
|
|
border-radius: 6px;
|
|
background:
|
|
radial-gradient(circle at 40% 0%, rgba(150, 255, 177, 0.12), transparent 60%),
|
|
linear-gradient(#122019, #09110f);
|
|
box-shadow: inset 0 0 34px rgba(90, 255, 136, 0.07), 0 14px 38px rgba(0,0,0,0.34);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.terminal p {
|
|
margin: 0 0 0.35rem;
|
|
}
|
|
|
|
.terminal .line {
|
|
min-height: 1.25em;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.meter {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: 0.45rem 0.8rem;
|
|
align-items: center;
|
|
margin-top: 0.65rem;
|
|
color: rgba(239, 228, 201, 0.82);
|
|
}
|
|
|
|
.bar {
|
|
height: 10px;
|
|
border: 1px solid rgba(239, 228, 201, 0.24);
|
|
border-radius: 999px;
|
|
overflow: hidden;
|
|
background: rgba(0,0,0,0.24);
|
|
}
|
|
|
|
.bar i {
|
|
display: block;
|
|
width: var(--w, 0%);
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--gold), #d9d0a2, #7fc798);
|
|
transition: width 360ms ease;
|
|
}
|
|
|
|
.rooms {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
.room {
|
|
position: absolute;
|
|
width: clamp(205px, 23vw, 310px);
|
|
min-height: 245px;
|
|
padding: 1rem 1rem 0.95rem;
|
|
border: 1px solid rgba(69, 52, 36, 0.36);
|
|
border-radius: 6px;
|
|
background:
|
|
linear-gradient(110deg, rgba(255,255,255,0.34), transparent 34% 60%, rgba(81,59,39,0.16)),
|
|
repeating-linear-gradient(0deg, transparent 0 16px, rgba(88,66,44,0.035) 17px),
|
|
var(--paper);
|
|
box-shadow: 0 14px 38px var(--shadow), inset 0 0 0 1px rgba(255,255,255,0.22);
|
|
transform: translate(var(--x), var(--y)) rotate(var(--r));
|
|
touch-action: none;
|
|
user-select: none;
|
|
transition: filter 300ms ease, box-shadow 300ms ease, background 300ms ease;
|
|
}
|
|
|
|
.room::before {
|
|
content: attr(data-mark);
|
|
position: absolute;
|
|
right: 0.85rem;
|
|
top: 0.7rem;
|
|
color: rgba(95, 70, 49, 0.2);
|
|
font: 900 2.2rem/1 "Courier New", monospace;
|
|
transform: rotate(7deg);
|
|
}
|
|
|
|
.room::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0.45rem;
|
|
pointer-events: none;
|
|
border: 1px solid rgba(95, 70, 49, 0.12);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.room.is-dragging {
|
|
z-index: 30;
|
|
filter: saturate(1.12) contrast(1.03);
|
|
box-shadow: 0 26px 70px rgba(0, 0, 0, 0.46), inset 0 0 0 1px rgba(255,255,255,0.35);
|
|
}
|
|
|
|
.room.is-awake {
|
|
animation: breathe 3.8s ease-in-out infinite;
|
|
}
|
|
|
|
.room.is-buried {
|
|
filter: grayscale(0.4) brightness(0.78);
|
|
}
|
|
|
|
.room.is-buried .memory {
|
|
opacity: 0.06;
|
|
}
|
|
|
|
.room h2 {
|
|
position: relative;
|
|
z-index: 1;
|
|
margin: 0 0 0.35rem;
|
|
color: var(--green);
|
|
font-size: 1.45rem;
|
|
font-weight: 500;
|
|
line-height: 1;
|
|
}
|
|
|
|
.room .code {
|
|
margin: 0 0 0.95rem;
|
|
color: rgba(95, 70, 49, 0.72);
|
|
font: 700 0.72rem/1 "Courier New", monospace;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.diagram {
|
|
position: relative;
|
|
height: 90px;
|
|
margin: 0.75rem 0;
|
|
border: 1px solid rgba(95, 70, 49, 0.18);
|
|
background:
|
|
radial-gradient(circle at var(--mx, 50%) var(--my, 50%), rgba(163,111,44,0.38), transparent 10%),
|
|
repeating-linear-gradient(90deg, rgba(35,29,24,0.06) 0 1px, transparent 1px 12px),
|
|
rgba(244, 236, 217, 0.72);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.diagram span {
|
|
position: absolute;
|
|
display: block;
|
|
border: 1px solid rgba(95, 70, 49, 0.42);
|
|
background: rgba(255, 255, 255, 0.18);
|
|
transition: transform 500ms ease, opacity 500ms ease;
|
|
}
|
|
|
|
.window span:nth-child(1) { left: 14%; top: 22%; width: 34%; height: 42%; }
|
|
.window span:nth-child(2) { right: 13%; top: 13%; width: 18%; height: 64%; }
|
|
.window span:nth-child(3) { left: 35%; bottom: 10%; width: 42%; height: 16%; }
|
|
.books span:nth-child(1) { left: 9%; bottom: 10%; width: 9%; height: 68%; }
|
|
.books span:nth-child(2) { left: 23%; bottom: 10%; width: 13%; height: 52%; }
|
|
.books span:nth-child(3) { left: 42%; bottom: 10%; width: 11%; height: 74%; }
|
|
.books span:nth-child(4) { left: 60%; bottom: 10%; width: 18%; height: 45%; }
|
|
.tape span:nth-child(1) { left: 14%; top: 26%; width: 26%; height: 46%; border-radius: 50%; }
|
|
.tape span:nth-child(2) { right: 14%; top: 26%; width: 26%; height: 46%; border-radius: 50%; }
|
|
.tape span:nth-child(3) { left: 28%; top: 46%; width: 44%; height: 11%; }
|
|
.stairs span:nth-child(1) { left: 14%; bottom: 13%; width: 18%; height: 18%; }
|
|
.stairs span:nth-child(2) { left: 31%; bottom: 30%; width: 18%; height: 18%; }
|
|
.stairs span:nth-child(3) { left: 48%; bottom: 47%; width: 18%; height: 18%; }
|
|
.stairs span:nth-child(4) { left: 65%; bottom: 64%; width: 18%; height: 18%; }
|
|
.table span:nth-child(1) { left: 17%; top: 26%; width: 66%; height: 12%; }
|
|
.table span:nth-child(2) { left: 26%; top: 38%; width: 9%; height: 38%; }
|
|
.table span:nth-child(3) { right: 26%; top: 38%; width: 9%; height: 38%; }
|
|
|
|
.memory {
|
|
position: relative;
|
|
z-index: 1;
|
|
min-height: 4.8rem;
|
|
margin: 0.75rem 0 0;
|
|
color: #35271d;
|
|
font-size: 0.96rem;
|
|
line-height: 1.42;
|
|
transition: opacity 400ms ease;
|
|
}
|
|
|
|
.room .stamp {
|
|
position: absolute;
|
|
right: 0.75rem;
|
|
bottom: 0.65rem;
|
|
width: 74px;
|
|
height: 74px;
|
|
border: 2px solid rgba(127, 51, 48, 0.45);
|
|
border-radius: 50%;
|
|
display: grid;
|
|
place-items: center;
|
|
color: rgba(127, 51, 48, 0.58);
|
|
font: 900 0.58rem/1.1 "Courier New", monospace;
|
|
text-align: center;
|
|
letter-spacing: 0.08em;
|
|
transform: rotate(-14deg) scale(0);
|
|
transition: transform 420ms cubic-bezier(.21,1.5,.45,1);
|
|
}
|
|
|
|
.room.is-indexed .stamp {
|
|
transform: rotate(-14deg) scale(1);
|
|
}
|
|
|
|
.slips {
|
|
position: absolute;
|
|
left: clamp(16px, 3vw, 40px);
|
|
bottom: clamp(16px, 3vw, 34px);
|
|
z-index: 18;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.55rem;
|
|
max-width: min(720px, calc(100vw - 2rem));
|
|
}
|
|
|
|
.slip {
|
|
width: 120px;
|
|
min-height: 52px;
|
|
padding: 0.55rem 0.6rem;
|
|
border: 1px solid rgba(95, 70, 49, 0.44);
|
|
border-radius: 3px;
|
|
background: var(--paper-3);
|
|
color: rgba(33, 27, 22, 0.55);
|
|
box-shadow: 0 8px 18px rgba(0,0,0,0.18);
|
|
font: 700 0.62rem/1.28 "Courier New", monospace;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
transform: rotate(var(--r));
|
|
transition: transform 300ms ease, color 300ms ease, background 300ms ease;
|
|
}
|
|
|
|
.slip.is-found {
|
|
color: var(--ink);
|
|
background: #fff4d6;
|
|
transform: rotate(var(--r)) translateY(-8px);
|
|
}
|
|
|
|
.drawer {
|
|
position: absolute;
|
|
right: clamp(18px, 4vw, 54px);
|
|
bottom: clamp(18px, 4vw, 46px);
|
|
z-index: 14;
|
|
width: min(360px, calc(100vw - 2rem));
|
|
min-height: 138px;
|
|
padding: 0.9rem;
|
|
border: 1px solid rgba(232, 221, 195, 0.26);
|
|
border-radius: 7px;
|
|
background:
|
|
linear-gradient(180deg, rgba(95,70,49,0.62), rgba(42,30,20,0.84)),
|
|
repeating-linear-gradient(90deg, rgba(255,255,255,0.04) 0 1px, transparent 1px 13px);
|
|
color: #eadfc2;
|
|
box-shadow: 0 16px 42px rgba(0,0,0,0.36);
|
|
}
|
|
|
|
.drawer h2 {
|
|
margin: 0 0 0.5rem;
|
|
font: 700 0.78rem/1.1 "Courier New", monospace;
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.drawer p {
|
|
margin: 0;
|
|
color: rgba(234, 223, 194, 0.76);
|
|
font-size: 0.92rem;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.drawer button,
|
|
.sound {
|
|
border: 1px solid rgba(232, 221, 195, 0.36);
|
|
border-radius: 4px;
|
|
background: rgba(244, 236, 217, 0.09);
|
|
color: #eadfc2;
|
|
padding: 0.45rem 0.6rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.drawer button:hover,
|
|
.sound:hover,
|
|
.drawer button:focus-visible,
|
|
.sound:focus-visible {
|
|
background: rgba(244, 236, 217, 0.2);
|
|
outline: 1px solid rgba(244, 236, 217, 0.5);
|
|
}
|
|
|
|
.drawer-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 0.75rem;
|
|
}
|
|
|
|
.sound {
|
|
position: absolute;
|
|
left: clamp(18px, 4vw, 54px);
|
|
top: clamp(18px, 4vw, 44px);
|
|
z-index: 24;
|
|
transform: translateY(138px);
|
|
font: 700 0.7rem/1 "Courier New", monospace;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.cursor {
|
|
position: fixed;
|
|
left: 0;
|
|
top: 0;
|
|
z-index: 60;
|
|
width: 32px;
|
|
height: 32px;
|
|
pointer-events: none;
|
|
transform: translate(-50%, -50%);
|
|
transition: width 200ms ease, height 200ms ease, filter 300ms ease;
|
|
}
|
|
|
|
.cursor::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 4px 9px 4px 12px;
|
|
border: 2px solid rgba(236, 206, 126, 0.95);
|
|
border-radius: 50% 50% 38% 38%;
|
|
box-shadow: 0 0 14px rgba(236, 206, 126, 0.45);
|
|
}
|
|
|
|
.cursor::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 17px;
|
|
top: 19px;
|
|
width: 18px;
|
|
height: 5px;
|
|
border-top: 2px solid rgba(236, 206, 126, 0.95);
|
|
border-right: 2px solid rgba(236, 206, 126, 0.95);
|
|
transform: rotate(25deg);
|
|
}
|
|
|
|
.cursor.is-waiting {
|
|
width: 46px;
|
|
height: 46px;
|
|
filter: drop-shadow(0 0 12px rgba(217, 208, 162, 0.65));
|
|
}
|
|
|
|
.word-catcher {
|
|
position: absolute;
|
|
left: -999px;
|
|
top: 0;
|
|
width: 1px;
|
|
height: 1px;
|
|
opacity: 0;
|
|
}
|
|
|
|
.typed {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: 1rem;
|
|
z-index: 70;
|
|
max-width: calc(100vw - 2rem);
|
|
padding: 0.45rem 0.65rem;
|
|
border: 1px solid rgba(232, 221, 195, 0.28);
|
|
border-radius: 4px;
|
|
background: rgba(15, 17, 15, 0.7);
|
|
color: #d9f0cf;
|
|
font: 0.78rem/1 "Courier New", monospace;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
transform: translateX(-50%);
|
|
opacity: 0;
|
|
transition: opacity 260ms ease;
|
|
}
|
|
|
|
.typed.is-visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
.ending {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 80;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 1rem;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
background:
|
|
radial-gradient(circle at 50% 42%, rgba(244,236,217,0.94), rgba(42,30,20,0.78) 58%, rgba(8,9,9,0.94));
|
|
transition: opacity 1400ms ease;
|
|
}
|
|
|
|
.ending.is-open {
|
|
pointer-events: auto;
|
|
opacity: 1;
|
|
}
|
|
|
|
.ending-card {
|
|
width: min(760px, 94vw);
|
|
padding: clamp(1.2rem, 5vw, 3rem);
|
|
border: 1px solid rgba(95, 70, 49, 0.34);
|
|
border-radius: 8px;
|
|
background:
|
|
linear-gradient(110deg, rgba(255,255,255,0.34), transparent 55%),
|
|
var(--paper-3);
|
|
box-shadow: 0 30px 90px rgba(0,0,0,0.5);
|
|
color: var(--ink);
|
|
transform: rotate(-0.8deg);
|
|
}
|
|
|
|
.ending-card h2 {
|
|
margin: 0 0 0.75rem;
|
|
color: var(--green);
|
|
font-size: clamp(2rem, 6vw, 4rem);
|
|
font-weight: 500;
|
|
line-height: 0.95;
|
|
}
|
|
|
|
.ending-card p {
|
|
max-width: 58ch;
|
|
margin: 0.85rem 0;
|
|
font-size: 1.05rem;
|
|
line-height: 1.52;
|
|
}
|
|
|
|
.ending-card button {
|
|
margin-top: 1rem;
|
|
border: 1px solid rgba(95, 70, 49, 0.42);
|
|
border-radius: 4px;
|
|
background: var(--green);
|
|
color: #f8efd9;
|
|
padding: 0.65rem 0.85rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.ghost-note {
|
|
position: absolute;
|
|
z-index: 16;
|
|
max-width: 220px;
|
|
padding: 0.6rem 0.7rem;
|
|
border: 1px solid rgba(95,70,49,0.18);
|
|
background: rgba(244,236,217,0.88);
|
|
color: rgba(33,27,22,0.8);
|
|
font-size: 0.9rem;
|
|
line-height: 1.32;
|
|
box-shadow: 0 12px 30px rgba(0,0,0,0.22);
|
|
opacity: 0;
|
|
transform: translateY(8px) rotate(var(--r));
|
|
animation: note 5.5s ease forwards;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.game.phase-dream .masthead {
|
|
transform: translateX(3vw) rotate(-1deg);
|
|
filter: blur(0.2px);
|
|
}
|
|
|
|
.game.phase-dream .desk {
|
|
background:
|
|
radial-gradient(circle at 48% 50%, rgba(38,56,74,0.3), transparent 34rem),
|
|
repeating-linear-gradient(90deg, rgba(244,236,217,0.04) 0 1px, transparent 1px 11px),
|
|
linear-gradient(145deg, rgba(49,67,72,0.28), rgba(20,17,14,0.34));
|
|
}
|
|
|
|
.game.phase-house .room {
|
|
background:
|
|
linear-gradient(110deg, rgba(255,255,255,0.34), transparent 34% 60%, rgba(81,59,39,0.16)),
|
|
repeating-linear-gradient(0deg, transparent 0 18px, rgba(88,66,44,0.04) 19px),
|
|
#ead8b7;
|
|
}
|
|
|
|
.game.phase-terminal .room {
|
|
filter: sepia(0.2) hue-rotate(40deg);
|
|
}
|
|
|
|
@media (max-width: 850px) {
|
|
.game {
|
|
min-height: 720px;
|
|
overflow: auto;
|
|
cursor: auto;
|
|
}
|
|
|
|
.cursor {
|
|
display: none;
|
|
}
|
|
|
|
.desk {
|
|
position: relative;
|
|
inset: auto;
|
|
min-height: 1080px;
|
|
margin: 0;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.masthead,
|
|
.machine,
|
|
.drawer,
|
|
.sound,
|
|
.slips {
|
|
position: relative;
|
|
left: auto;
|
|
right: auto;
|
|
top: auto;
|
|
bottom: auto;
|
|
width: auto;
|
|
max-width: none;
|
|
margin: 1rem;
|
|
transform: none;
|
|
}
|
|
|
|
.machine {
|
|
z-index: 25;
|
|
}
|
|
|
|
.rooms {
|
|
position: relative;
|
|
display: grid;
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.room {
|
|
position: relative;
|
|
width: auto;
|
|
min-height: 235px;
|
|
transform: rotate(var(--r));
|
|
}
|
|
|
|
.zones {
|
|
display: none;
|
|
}
|
|
|
|
.slips {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.slip {
|
|
width: auto;
|
|
}
|
|
}
|
|
|
|
@keyframes rain {
|
|
from { background-position: 0 0, 0 0, 0 0; }
|
|
to { background-position: 0 42px, 0 0, 0 0; }
|
|
}
|
|
|
|
@keyframes breathe {
|
|
0%, 100% { transform: translate(var(--x), var(--y)) rotate(var(--r)) scale(1); }
|
|
50% { transform: translate(var(--x), var(--y)) rotate(var(--r)) scale(1.014); }
|
|
}
|
|
|
|
@keyframes note {
|
|
12%, 72% { opacity: 1; transform: translateY(0) rotate(var(--r)); }
|
|
100% { opacity: 0; transform: translateY(-18px) rotate(var(--r)); }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.game,
|
|
.game *,
|
|
.game *::before,
|
|
.game *::after {
|
|
animation-duration: 0.001ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
scroll-behavior: auto !important;
|
|
transition-duration: 0.001ms !important;
|
|
}
|
|
}
|
|
</style>
|
|
<main class="game" id="game" aria-label="The Rain Index">
|
|
<input class="word-catcher" id="wordCatcher" aria-label="archive input" autocomplete="off" spellcheck="false">
|
|
<div class="desk" id="desk">
|
|
<div class="zones" aria-hidden="true">
|
|
<div class="zone" data-zone="attic"><span>attic weather</span></div>
|
|
<div class="zone" data-zone="terminal"><span>terminal dusk</span></div>
|
|
<div class="zone" data-zone="kitchen"><span>kitchen witness</span></div>
|
|
<div class="zone" data-zone="stair"><span>stair memory</span></div>
|
|
</div>
|
|
|
|
<header class="masthead">
|
|
<small>Department of Unfinished Domestic Topographies</small>
|
|
<h1>The Rain Index</h1>
|
|
<p class="no-explain">The exhibit accepts handling. The catalogue dislikes certainty.</p>
|
|
</header>
|
|
|
|
<button class="sound" id="soundBtn" type="button" aria-pressed="false">sound</button>
|
|
|
|
<aside class="machine" aria-live="polite">
|
|
<div class="terminal" id="terminal">
|
|
<p>RAIN-INDEX / teaching copy / wet boot sector</p>
|
|
<div class="line" id="lineA">specimens asleep behind paper.</div>
|
|
<div class="line" id="lineB">move a room. wait over it. type what returns.</div>
|
|
<div class="line" id="lineC"></div>
|
|
</div>
|
|
<div class="meter">
|
|
<span>catalogue humidity</span>
|
|
<span id="humidityText">0%</span>
|
|
<div class="bar" aria-hidden="true"><i id="humidityBar"></i></div>
|
|
<span id="visitText">first attendance</span>
|
|
</div>
|
|
</aside>
|
|
|
|
<section class="rooms" id="rooms" aria-label="movable archive rooms">
|
|
<article class="room" data-id="window" data-mark="I" data-word="rain" style="--x:15vw;--y:31vh;--r:-2deg">
|
|
<h2>South Window</h2>
|
|
<p class="code">specimen 04 / condensation script</p>
|
|
<div class="diagram window" aria-hidden="true"><span></span><span></span><span></span></div>
|
|
<p class="memory">The glass keeps a list of people who looked out instead of answering.</p>
|
|
<span class="stamp">not yet<br>indexed</span>
|
|
</article>
|
|
|
|
<article class="room" data-id="library" data-mark="II" data-word="borge" style="--x:39vw;--y:39vh;--r:1.5deg">
|
|
<h2>Borrowed Library</h2>
|
|
<p class="code">specimen 11 / recursive shelf</p>
|
|
<div class="diagram books" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
|
<p class="memory">Every book is filed under the title it had in someone else's dream.</p>
|
|
<span class="stamp">not yet<br>indexed</span>
|
|
</article>
|
|
|
|
<article class="room" data-id="tape" data-mark="III" data-word="mother" style="--x:62vw;--y:28vh;--r:-1deg">
|
|
<h2>Cassette Kitchen</h2>
|
|
<p class="code">specimen 19 / domestic magnetic</p>
|
|
<div class="diagram tape" aria-hidden="true"><span></span><span></span><span></span></div>
|
|
<p class="memory">A spoon taps the cup twice. The tape records the third tap before it happens.</p>
|
|
<span class="stamp">not yet<br>indexed</span>
|
|
</article>
|
|
|
|
<article class="room" data-id="stairs" data-mark="IV" data-word="below" style="--x:22vw;--y:61vh;--r:1deg">
|
|
<h2>Reverse Stairwell</h2>
|
|
<p class="code">specimen 27 / impossible municipal</p>
|
|
<div class="diagram stairs" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
|
<p class="memory">Down is a rumor told by upstairs to make itself feel necessary.</p>
|
|
<span class="stamp">not yet<br>indexed</span>
|
|
</article>
|
|
|
|
<article class="room" data-id="desklet" data-mark="V" data-word="zaine" style="--x:51vw;--y:63vh;--r:-1.8deg">
|
|
<h2>Young Desk</h2>
|
|
<p class="code">specimen 32 / pencil pressure</p>
|
|
<div class="diagram table" aria-hidden="true"><span></span><span></span><span></span></div>
|
|
<p class="memory">The drawer contains a map of the room drawn from under the table.</p>
|
|
<span class="stamp">not yet<br>indexed</span>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="slips" id="slips" aria-label="index slips">
|
|
<div class="slip" data-slip="window" style="--r:-1deg">I<br>window missing</div>
|
|
<div class="slip" data-slip="library" style="--r:1.4deg">II<br>shelf missing</div>
|
|
<div class="slip" data-slip="tape" style="--r:-2deg">III<br>voice missing</div>
|
|
<div class="slip" data-slip="stairs" style="--r:1deg">IV<br>stair missing</div>
|
|
<div class="slip" data-slip="desklet" style="--r:-1.3deg">V<br>name missing</div>
|
|
</section>
|
|
|
|
<aside class="drawer" id="drawer">
|
|
<h2>card drawer pretending to be closed</h2>
|
|
<p id="drawerText">This drawer opens when you close it. It closes when you trust it.</p>
|
|
<div class="drawer-actions">
|
|
<button id="closeDrawer" type="button">close</button>
|
|
<button id="misfile" type="button">misfile</button>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
<div class="cursor" id="cursor" aria-hidden="true"></div>
|
|
<div class="typed" id="typed" aria-hidden="true"></div>
|
|
<section class="ending" id="ending" aria-modal="true" aria-labelledby="endingTitle">
|
|
<div class="ending-card">
|
|
<h2 id="endingTitle">The House Files You Back</h2>
|
|
<p id="endingCopy">The rain has found its alphabetical order. A small room opens in the index where your visit has been carefully misremembered.</p>
|
|
<p>The page will remember that you came here. Next time, one sentence will be waiting before you touch anything.</p>
|
|
<button id="again" type="button">leave the drawer slightly open</button>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<script>
|
|
(() => {
|
|
document.body.classList.add("rain-index-page");
|
|
document.getElementById("content")?.classList.add("rain-index-content");
|
|
|
|
const game = document.getElementById("game");
|
|
const desk = document.getElementById("desk");
|
|
const roomsWrap = document.getElementById("rooms");
|
|
const rooms = Array.from(document.querySelectorAll(".room"));
|
|
const zones = Array.from(document.querySelectorAll(".zone"));
|
|
const cursor = document.getElementById("cursor");
|
|
const typed = document.getElementById("typed");
|
|
const input = document.getElementById("wordCatcher");
|
|
const lineA = document.getElementById("lineA");
|
|
const lineB = document.getElementById("lineB");
|
|
const lineC = document.getElementById("lineC");
|
|
const humidityBar = document.getElementById("humidityBar");
|
|
const humidityText = document.getElementById("humidityText");
|
|
const visitText = document.getElementById("visitText");
|
|
const drawerText = document.getElementById("drawerText");
|
|
const closeDrawer = document.getElementById("closeDrawer");
|
|
const misfile = document.getElementById("misfile");
|
|
const soundBtn = document.getElementById("soundBtn");
|
|
const ending = document.getElementById("ending");
|
|
const endingCopy = document.getElementById("endingCopy");
|
|
const again = document.getElementById("again");
|
|
const storageKey = "rainIndexState";
|
|
const phrases = {
|
|
attic: [
|
|
"attic weather has entered the catalogue.",
|
|
"a drip begins in a room with no ceiling.",
|
|
"the window alphabetizes the rain by surname."
|
|
],
|
|
terminal: [
|
|
"terminal dusk accepts the specimen.",
|
|
"green letters gather at the edge of the paper.",
|
|
"the old lab computer asks for a childhood answer."
|
|
],
|
|
kitchen: [
|
|
"kitchen witness confirms a domestic anomaly.",
|
|
"someone rinses a cup in the next decade.",
|
|
"the cassette smells faintly of warm dust."
|
|
],
|
|
stair: [
|
|
"stair memory folds the room through itself.",
|
|
"an impossible landing briefly becomes plausible.",
|
|
"the archive miscounts the floors and apologizes."
|
|
]
|
|
};
|
|
const wordSecrets = {
|
|
rain: "The window replies in water: not weather, but handwriting.",
|
|
borge: "A shelf turns inward. For one second, every title is the title of this page.",
|
|
mother: "The tape softens. A voice nearly says your name, then chooses the kettle.",
|
|
below: "The stairwell lowers the ceiling and raises the past.",
|
|
zaine: "The desk recognizes its owner without making a scene.",
|
|
library: "A librarian with no face stamps the inside of your cursor.",
|
|
sleep: "All rooms become lighter when they pretend not to hear you.",
|
|
home: "The drawer refuses the word, then keeps it."
|
|
};
|
|
const slipNames = {
|
|
window: "I\nrain indexed",
|
|
library: "II\nshelf indexed",
|
|
tape: "III\nvoice indexed",
|
|
stairs: "IV\nstair indexed",
|
|
desklet: "V\nname indexed"
|
|
};
|
|
const visits = Number(localStorage.getItem("rainIndexVisits") || "0") + 1;
|
|
let state = loadState();
|
|
let pointer = { x: innerWidth / 2, y: innerHeight / 2 };
|
|
let heldRoom = null;
|
|
let drag = null;
|
|
let zCounter = 30;
|
|
let typedBuffer = "";
|
|
let typeTimer = 0;
|
|
let stillTimer = 0;
|
|
let lastMove = Date.now();
|
|
let audio = null;
|
|
let drone = null;
|
|
let tick = null;
|
|
|
|
localStorage.setItem("rainIndexVisits", String(visits));
|
|
visitText.textContent = visits === 1 ? "first attendance" : `attendance ${visits}`;
|
|
if (visits > 1) {
|
|
lineC.textContent = "previous visit found under blotter.";
|
|
spawnNote("You have been here in a different order.", 48, 18);
|
|
}
|
|
|
|
applyState();
|
|
updateHumidity();
|
|
input.focus({ preventScroll: true });
|
|
|
|
document.addEventListener("pointermove", onPointerMove);
|
|
document.addEventListener("pointerdown", () => input.focus({ preventScroll: true }));
|
|
document.addEventListener("keydown", onKey);
|
|
window.addEventListener("resize", resizeReality);
|
|
closeDrawer.addEventListener("click", drawerClose);
|
|
misfile.addEventListener("click", misfileRooms);
|
|
soundBtn.addEventListener("click", toggleSound);
|
|
again.addEventListener("click", restart);
|
|
|
|
rooms.forEach((room) => {
|
|
room.addEventListener("pointerdown", startDrag);
|
|
room.addEventListener("pointerenter", () => {
|
|
heldRoom = room;
|
|
room.classList.add("is-awake");
|
|
lineB.textContent = `specimen ${room.dataset.mark} is warm at the edge.`;
|
|
});
|
|
room.addEventListener("pointerleave", () => {
|
|
if (!drag || drag.room !== room) room.classList.remove("is-awake");
|
|
if (heldRoom === room) heldRoom = null;
|
|
});
|
|
room.addEventListener("dblclick", () => {
|
|
reveal(room, "double handling produced unauthorized certainty.");
|
|
});
|
|
});
|
|
|
|
requestAnimationFrame(loop);
|
|
|
|
function loadState() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(storageKey)) || { found: [], ending: 0 };
|
|
} catch {
|
|
return { found: [], ending: 0 };
|
|
}
|
|
}
|
|
|
|
function saveState() {
|
|
localStorage.setItem(storageKey, JSON.stringify(state));
|
|
}
|
|
|
|
function applyState() {
|
|
rooms.forEach((room) => {
|
|
if (state.found.includes(room.dataset.id)) {
|
|
room.classList.add("is-indexed");
|
|
room.querySelector(".stamp").innerHTML = "indexed<br>in rain";
|
|
}
|
|
});
|
|
document.querySelectorAll(".slip").forEach((slip) => {
|
|
if (state.found.includes(slip.dataset.slip)) {
|
|
slip.classList.add("is-found");
|
|
slip.textContent = slipNames[slip.dataset.slip];
|
|
}
|
|
});
|
|
}
|
|
|
|
function onPointerMove(event) {
|
|
pointer = { x: event.clientX, y: event.clientY };
|
|
cursor.style.left = `${pointer.x}px`;
|
|
cursor.style.top = `${pointer.y}px`;
|
|
lastMove = Date.now();
|
|
game.classList.add("has-touched");
|
|
if (drag) moveDrag(event);
|
|
const rect = desk.getBoundingClientRect();
|
|
const mx = `${((pointer.x - rect.left) / rect.width) * 100}%`;
|
|
const my = `${((pointer.y - rect.top) / rect.height) * 100}%`;
|
|
rooms.forEach((room) => {
|
|
const r = room.getBoundingClientRect();
|
|
if (pointer.x >= r.left && pointer.x <= r.right && pointer.y >= r.top && pointer.y <= r.bottom) {
|
|
room.querySelector(".diagram").style.setProperty("--mx", mx);
|
|
room.querySelector(".diagram").style.setProperty("--my", my);
|
|
}
|
|
});
|
|
}
|
|
|
|
function startDrag(event) {
|
|
const room = event.currentTarget;
|
|
const rect = room.getBoundingClientRect();
|
|
drag = {
|
|
room,
|
|
startX: event.clientX,
|
|
startY: event.clientY,
|
|
baseX: rect.left - desk.getBoundingClientRect().left,
|
|
baseY: rect.top - desk.getBoundingClientRect().top
|
|
};
|
|
room.classList.add("is-dragging");
|
|
room.style.zIndex = String(++zCounter);
|
|
room.setPointerCapture(event.pointerId);
|
|
room.addEventListener("pointerup", endDrag, { once: true });
|
|
room.addEventListener("pointercancel", endDrag, { once: true });
|
|
lineA.textContent = "manual rearrangement detected.";
|
|
tickSound(220, 0.03);
|
|
}
|
|
|
|
function moveDrag(event) {
|
|
const x = drag.baseX + event.clientX - drag.startX;
|
|
const y = drag.baseY + event.clientY - drag.startY;
|
|
drag.room.style.setProperty("--x", `${x}px`);
|
|
drag.room.style.setProperty("--y", `${y}px`);
|
|
lightZone(event.clientX, event.clientY);
|
|
}
|
|
|
|
function endDrag(event) {
|
|
const room = event.currentTarget;
|
|
room.classList.remove("is-dragging");
|
|
const zone = getZone(pointer.x, pointer.y);
|
|
if (zone) {
|
|
const text = pick(phrases[zone.dataset.zone]);
|
|
lineA.textContent = text;
|
|
mutateRoom(room, zone.dataset.zone);
|
|
if (zone.dataset.zone === "terminal" && room.dataset.id === "library") reveal(room, "the library prefers phosphor.");
|
|
if (zone.dataset.zone === "kitchen" && room.dataset.id === "tape") reveal(room, "the kitchen heard itself on tape.");
|
|
if (zone.dataset.zone === "stair" && room.dataset.id === "stairs") reveal(room, "the stairwell agreed to be counted.");
|
|
if (zone.dataset.zone === "attic" && room.dataset.id === "window") reveal(room, "rain entered from below.");
|
|
if (zone.dataset.zone === "kitchen" && room.dataset.id === "desklet") reveal(room, "the desk remembered a table.");
|
|
}
|
|
zones.forEach((z) => z.classList.remove("is-lit"));
|
|
drag = null;
|
|
tickSound(165, 0.04);
|
|
}
|
|
|
|
function getZone(x, y) {
|
|
return zones.find((zone) => {
|
|
const r = zone.getBoundingClientRect();
|
|
return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
|
|
});
|
|
}
|
|
|
|
function lightZone(x, y) {
|
|
const active = getZone(x, y);
|
|
zones.forEach((zone) => zone.classList.toggle("is-lit", zone === active));
|
|
}
|
|
|
|
function mutateRoom(room, zone) {
|
|
const diagram = room.querySelector(".diagram");
|
|
const parts = Array.from(diagram.children);
|
|
parts.forEach((part, index) => {
|
|
const sign = zone === "stair" || zone === "terminal" ? -1 : 1;
|
|
part.style.transform = `translate(${sign * (index + 1) * 3}px, ${(index % 2 ? -1 : 1) * 4}px)`;
|
|
part.style.opacity = zone === "attic" ? "0.62" : "1";
|
|
});
|
|
game.classList.remove("phase-dream", "phase-house", "phase-terminal");
|
|
if (zone === "stair") game.classList.add("phase-dream");
|
|
if (zone === "kitchen") game.classList.add("phase-house");
|
|
if (zone === "terminal") game.classList.add("phase-terminal");
|
|
}
|
|
|
|
function onKey(event) {
|
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
if (event.key === "Escape") {
|
|
typedBuffer = "";
|
|
showTyped("");
|
|
return;
|
|
}
|
|
if (event.key.length !== 1) return;
|
|
typedBuffer = (typedBuffer + event.key.toLowerCase()).replace(/[^a-z]/g, "").slice(-16);
|
|
showTyped(typedBuffer);
|
|
clearTimeout(typeTimer);
|
|
typeTimer = setTimeout(() => showTyped(""), 1600);
|
|
Object.keys(wordSecrets).forEach((word) => {
|
|
if (typedBuffer.endsWith(word)) {
|
|
lineC.textContent = wordSecrets[word];
|
|
const room = rooms.find((item) => item.dataset.word === word);
|
|
if (room) reveal(room, wordSecrets[word]);
|
|
typedBuffer = "";
|
|
showTyped("");
|
|
tickSound(440, 0.06);
|
|
}
|
|
});
|
|
}
|
|
|
|
function showTyped(text) {
|
|
typed.textContent = text ? `>${text}` : "";
|
|
typed.classList.toggle("is-visible", Boolean(text));
|
|
}
|
|
|
|
function reveal(room, reason) {
|
|
const id = room.dataset.id;
|
|
if (!state.found.includes(id)) {
|
|
state.found.push(id);
|
|
saveState();
|
|
room.classList.add("is-indexed");
|
|
room.querySelector(".stamp").innerHTML = "indexed<br>in rain";
|
|
const slip = document.querySelector(`[data-slip="${id}"]`);
|
|
if (slip) {
|
|
slip.classList.add("is-found");
|
|
slip.textContent = slipNames[id];
|
|
}
|
|
lineA.textContent = reason;
|
|
lineB.textContent = `${state.found.length}/5 catalogue slips recovered.`;
|
|
spawnNote(pick([
|
|
"A card slips out from behind the room.",
|
|
"Somebody wrote this down before you arrived.",
|
|
"The paper is briefly warmer than the screen."
|
|
]), pointer.x, pointer.y);
|
|
updateHumidity();
|
|
if (state.found.length >= 5) finish();
|
|
} else {
|
|
lineB.textContent = "already indexed, but not exhausted.";
|
|
}
|
|
}
|
|
|
|
function updateHumidity() {
|
|
const humidity = Math.min(100, Math.round((state.found.length / 5) * 100));
|
|
humidityText.textContent = `${humidity}%`;
|
|
humidityBar.style.setProperty("--w", `${humidity}%`);
|
|
}
|
|
|
|
function drawerClose() {
|
|
drawerText.textContent = pick([
|
|
"The closed drawer is open on the inside.",
|
|
"A brass label changes from SUBJECT to VISITOR.",
|
|
"You hear index cards settling into impossible order."
|
|
]);
|
|
rooms.forEach((room, index) => {
|
|
if ((index + state.found.length) % 2 === 0) room.classList.toggle("is-buried");
|
|
});
|
|
lineC.textContent = "conventional interface expectation broken politely.";
|
|
if (state.found.length >= 3) {
|
|
const unrevealed = rooms.find((room) => !state.found.includes(room.dataset.id));
|
|
if (unrevealed) reveal(unrevealed, "the drawer closed around a missing card.");
|
|
}
|
|
tickSound(110, 0.05);
|
|
}
|
|
|
|
function misfileRooms() {
|
|
const deskRect = desk.getBoundingClientRect();
|
|
rooms.forEach((room) => {
|
|
const x = 70 + Math.random() * Math.max(160, deskRect.width - 420);
|
|
const y = 170 + Math.random() * Math.max(160, deskRect.height - 420);
|
|
room.style.setProperty("--x", `${x}px`);
|
|
room.style.setProperty("--y", `${y}px`);
|
|
room.style.setProperty("--r", `${-3 + Math.random() * 6}deg`);
|
|
room.classList.remove("is-buried");
|
|
});
|
|
lineA.textContent = "classification failure made several new truths possible.";
|
|
spawnNote("The archive prefers a useful mistake.", pointer.x, pointer.y);
|
|
}
|
|
|
|
function resizeReality() {
|
|
if (innerWidth < 851) return;
|
|
lineB.textContent = innerWidth > innerHeight ? "wide room: the archive remembers maps." : "tall room: the archive remembers wells.";
|
|
if (Math.abs(innerWidth - innerHeight) < 90) {
|
|
const room = rooms.find((item) => item.dataset.id === "desklet");
|
|
if (room) reveal(room, "the square window admitted a private name.");
|
|
}
|
|
}
|
|
|
|
function loop() {
|
|
const waiting = Date.now() - lastMove > 1700;
|
|
cursor.classList.toggle("is-waiting", waiting);
|
|
if (waiting && heldRoom && !drag) {
|
|
stillTimer += 1;
|
|
if (stillTimer > 80) {
|
|
stillTimer = 0;
|
|
heldRoom.querySelector(".memory").textContent = developMemory(heldRoom);
|
|
if (Math.random() > 0.46) reveal(heldRoom, "a patient hover developed the image.");
|
|
}
|
|
} else {
|
|
stillTimer = 0;
|
|
}
|
|
requestAnimationFrame(loop);
|
|
}
|
|
|
|
function developMemory(room) {
|
|
const pool = {
|
|
window: [
|
|
"On the other side of the glass, someone is trying to remember this room into being.",
|
|
"The rain falls upward whenever nobody uses the word weather."
|
|
],
|
|
library: [
|
|
"A catalogue card describes a catalogue card describing your hand.",
|
|
"One book contains a floorplan. The floorplan contains a smaller library."
|
|
],
|
|
tape: [
|
|
"The cassette reverses and plays the sound of a chair being forgiven.",
|
|
"A recipe is read aloud, but all ingredients are dates."
|
|
],
|
|
stairs: [
|
|
"The fourth step is only visible from the fifth.",
|
|
"A landing opens into a lecture hall where the chalk is still raining."
|
|
],
|
|
desklet: [
|
|
"A pencil line moves by itself, then pretends to be a scratch.",
|
|
"The drawer has kept every failed password as a pressed flower."
|
|
]
|
|
};
|
|
return pick(pool[room.dataset.id]);
|
|
}
|
|
|
|
function spawnNote(text, x, y) {
|
|
const note = document.createElement("div");
|
|
note.className = "ghost-note";
|
|
note.textContent = text;
|
|
note.style.left = `${Math.min(Math.max(x - 40, 16), innerWidth - 250)}px`;
|
|
note.style.top = `${Math.min(Math.max(y - 20, 70), innerHeight - 160)}px`;
|
|
note.style.setProperty("--r", `${-3 + Math.random() * 6}deg`);
|
|
desk.appendChild(note);
|
|
setTimeout(() => note.remove(), 5600);
|
|
}
|
|
|
|
function finish() {
|
|
if (ending.classList.contains("is-open")) return;
|
|
state.ending = (state.ending || 0) + 1;
|
|
saveState();
|
|
endingCopy.textContent = state.ending > 1
|
|
? "The index recognizes the shape of your return. This time, it files the rain under Almost Home."
|
|
: "The rain has found its alphabetical order. A small room opens in the index where your visit has been carefully misremembered.";
|
|
setTimeout(() => ending.classList.add("is-open"), 900);
|
|
if (audio) {
|
|
tickSound(523, 0.08);
|
|
tickSound(659, 0.08, 0.14);
|
|
}
|
|
}
|
|
|
|
function restart() {
|
|
ending.classList.remove("is-open");
|
|
state.found = [];
|
|
saveState();
|
|
rooms.forEach((room) => {
|
|
room.classList.remove("is-indexed", "is-buried");
|
|
room.querySelector(".stamp").innerHTML = "not yet<br>indexed";
|
|
});
|
|
document.querySelectorAll(".slip").forEach((slip) => {
|
|
slip.classList.remove("is-found");
|
|
slip.textContent = `${slip.dataset.slip === "window" ? "I" : slip.dataset.slip === "library" ? "II" : slip.dataset.slip === "tape" ? "III" : slip.dataset.slip === "stairs" ? "IV" : "V"}\nmissing`;
|
|
});
|
|
updateHumidity();
|
|
misfileRooms();
|
|
}
|
|
|
|
function toggleSound() {
|
|
if (!audio) {
|
|
audio = new (window.AudioContext || window.webkitAudioContext)();
|
|
const gain = audio.createGain();
|
|
gain.gain.value = 0.028;
|
|
drone = audio.createOscillator();
|
|
drone.type = "sine";
|
|
drone.frequency.value = 56;
|
|
drone.connect(gain).connect(audio.destination);
|
|
drone.start();
|
|
tick = audio.createGain();
|
|
tick.gain.value = 0.08;
|
|
tick.connect(audio.destination);
|
|
soundBtn.setAttribute("aria-pressed", "true");
|
|
soundBtn.textContent = "sound on";
|
|
lineC.textContent = "rain carrier enabled.";
|
|
} else {
|
|
audio.close();
|
|
audio = null;
|
|
drone = null;
|
|
tick = null;
|
|
soundBtn.setAttribute("aria-pressed", "false");
|
|
soundBtn.textContent = "sound";
|
|
}
|
|
}
|
|
|
|
function tickSound(freq, volume, delay = 0) {
|
|
if (!audio) return;
|
|
const osc = audio.createOscillator();
|
|
const gain = audio.createGain();
|
|
osc.type = "triangle";
|
|
osc.frequency.value = freq;
|
|
gain.gain.value = volume;
|
|
osc.connect(gain).connect(audio.destination);
|
|
const now = audio.currentTime + delay;
|
|
gain.gain.setValueAtTime(volume, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
|
|
osc.start(now);
|
|
osc.stop(now + 0.2);
|
|
}
|
|
|
|
function pick(items) {
|
|
return items[Math.floor(Math.random() * items.length)];
|
|
}
|
|
})();
|
|
</script>
|
|
#+END_EXPORT
|