Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# enhanced image cache (generated at runtime)
/public/enhanced-cache/*.png
14 changes: 13 additions & 1 deletion app/app/api/enhance-image/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { NextRequest, NextResponse } from "next/server";
import { generateImageFromBase64 } from "@/app/utils/gemini";
import { writeFile, mkdir } from "fs/promises";
import { join } from "path";

export async function POST(req: NextRequest) {
const { imageDataUrl } = await req.json();
const { imageDataUrl, locationKey } = await req.json();
const base64 = imageDataUrl.replace(/^data:image\/\w+;base64,/, "");
const result = await generateImageFromBase64(
base64,
"This image is blurry. Can you please clear it up? Make it feel a bit darker and gloomier.",
);

// Persist to disk so the cache survives server restarts.
if (locationKey && result.imageDataUrl) {
const imgBase64 = result.imageDataUrl.replace(/^data:image\/\w+;base64,/, "");
const buffer = Buffer.from(imgBase64, "base64");
const cacheDir = join(process.cwd(), "public", "enhanced-cache");
await mkdir(cacheDir, { recursive: true });
await writeFile(join(cacheDir, `${locationKey}.png`), buffer);
}

return NextResponse.json({ imageDataUrl: result.imageDataUrl });
}
113 changes: 113 additions & 0 deletions app/app/game/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use client";

import Link from "next/link";

export default function LandingPage() {
return (
<main className="relative h-screen w-screen overflow-hidden bg-black flex flex-col items-center justify-center select-none">

{/* Film grain */}
<div className="grain-overlay" aria-hidden="true" />

{/* CRT scanlines */}
<div className="scanlines-overlay" aria-hidden="true" />

{/* Radial vignette — darkens the edges */}
<div
className="pointer-events-none absolute inset-0 z-10"
style={{
background:
"radial-gradient(ellipse at center, transparent 25%, rgba(0,0,0,0.75) 100%)",
}}
aria-hidden="true"
/>

{/* Ambient red glow behind the title */}
<div
className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-0"
style={{
width: "70vw",
height: "50vh",
background:
"radial-gradient(ellipse at center, rgba(120, 0, 0, 0.18) 0%, transparent 70%)",
filter: "blur(48px)",
}}
aria-hidden="true"
/>

{/* ── Main content ─────────────────────────────────────────── */}
<div className="relative z-20 flex flex-col items-center text-center px-6">

{/* Classification badge */}
<p
className="font-mono text-red-950 text-xs tracking-[0.6em] uppercase"
style={{ animation: "fade-in-up 0.8s ease 0.15s both" }}
>
INCIDENT REPORT ████&nbsp;&nbsp;·&nbsp;&nbsp;RESTRICTED ACCESS
</p>

{/* Title */}
<h1
className="font-mono font-black text-white leading-none uppercase mt-4"
style={{
fontSize: "clamp(5.5rem, 19vw, 21rem)",
letterSpacing: "-0.03em",
animation:
"fade-in-up 1s ease 0.5s both, flicker 9s ease-in-out 4s infinite, glitch 7s ease-in-out 5s infinite",
}}
>
GET OUT
</h1>

{/* Hairline separator */}
<div
className="w-48 h-px mt-5"
style={{
background:
"linear-gradient(to right, transparent, rgba(153,27,27,0.8), transparent)",
animation: "fade-in-up 0.8s ease 0.9s both",
}}
/>

{/* Tagline */}
<p
className="font-mono text-red-700 text-xs md:text-sm tracking-[0.45em] uppercase mt-5"
style={{ animation: "fade-in-up 0.8s ease 1.1s both" }}
>
No story.&nbsp;&nbsp;No plot armour.&nbsp;&nbsp;No guarantee.
</p>

{/* Description */}
<div
className="mt-6 font-mono text-zinc-600 text-sm leading-loose space-y-0.5"
style={{ animation: "fade-in-up 0.8s ease 1.4s both" }}
>
<p>Kyle is trapped inside. You are his only hope.</p>
<p>Speak. He listens. Guide him to safety.</p>
<p>Every choice reshapes the world. Every playthrough is different.</p>
</div>

{/* CTA button */}
<Link href="/" className="mt-10">
<button
className="font-mono text-xs tracking-[0.5em] uppercase border border-red-900 text-red-600 px-14 py-4 transition-all duration-500 hover:bg-red-950/60 hover:text-white hover:border-red-700 hover:tracking-[0.6em] active:scale-95"
style={{
animation:
"fade-in-up 0.8s ease 1.8s both, pulse-glow 3s ease-in-out 2.8s infinite",
}}
>
ENTER THE ROOM &rarr;
</button>
</Link>

{/* Footer */}
<p
className="mt-10 font-mono text-zinc-800 text-xs tracking-widest uppercase"
style={{ animation: "fade-in-up 0.8s ease 2.2s both" }}
>
⚠&nbsp;&nbsp;No known survivors have been documented
</p>
</div>
</main>
);
}
134 changes: 125 additions & 9 deletions app/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
@import "tailwindcss";

:root {
--background: #ffffff;
--foreground: #171717;
--background: #000000;
--foreground: #ededed;
}

@theme inline {
Expand All @@ -12,15 +12,131 @@
--font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

/* ─── Landing page animations ─────────────────────────────────────────────── */

@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(28px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

/* Occasional flicker on the title — subtle, not seizure-inducing */
@keyframes flicker {
0%,
94%,
96%,
100% {
opacity: 1;
}
95% {
opacity: 0.75;
}
97% {
opacity: 0.6;
}
98%,
99% {
opacity: 0.9;
}
}

/* Chromatic-aberration glitch that fires every ~7s */
@keyframes glitch {
0%,
82%,
100% {
text-shadow: none;
transform: skewX(0deg) translateX(0);
}
83% {
text-shadow: 0.05em 0 0 rgba(220, 38, 38, 0.85),
-0.05em 0 0 rgba(0, 210, 255, 0.45);
transform: skewX(-2deg) translateX(-4px);
}
85% {
text-shadow: -0.05em 0 0 rgba(220, 38, 38, 0.85),
0.05em 0 0 rgba(0, 210, 255, 0.45);
transform: skewX(1.5deg) translateX(4px);
}
87% {
text-shadow: 0.02em 0 0 rgba(220, 38, 38, 0.6);
transform: skewX(0deg) translateX(-2px);
}
89% {
text-shadow: none;
transform: none;
}
}

/* Pulsing red glow on the CTA button */
@keyframes pulse-glow {
0%,
100% {
box-shadow: 0 0 8px rgba(153, 27, 27, 0.3);
}
50% {
box-shadow: 0 0 28px rgba(153, 27, 27, 0.65),
0 0 60px rgba(153, 27, 27, 0.2);
}
}

/* Film grain — SVG noise tile shifts position every frame */
@keyframes grain {
0%,
100% {
transform: translate(0, 0);
}
20% {
transform: translate(-6%, -10%);
}
40% {
transform: translate(8%, 6%);
}
60% {
transform: translate(-4%, 12%);
}
80% {
transform: translate(10%, -8%);
}
}

/* ─── Landing page overlays ───────────────────────────────────────────────── */

/* Film grain */
.grain-overlay {
position: fixed;
inset: -50%;
width: 200%;
height: 200%;
z-index: 5;
pointer-events: none;
opacity: 0.045;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='200' height='200' filter='url(%23n)'/></svg>");
animation: grain 0.7s steps(2) infinite;
}

/* CRT scanlines */
.scanlines-overlay {
position: fixed;
inset: 0;
z-index: 6;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
transparent,
transparent 3px,
rgba(0, 0, 0, 0.06) 3px,
rgba(0, 0, 0, 0.06) 4px
);
}
Loading