<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oneko Runner</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: monospace;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
overflow-x: hidden;
}
/* ── The main neon line ── */
.runner-track {
position: fixed;
bottom: 80px;
left: 0;
width: 100%;
height: 60px;
background: linear-gradient(90deg, transparent 0%, #ff006e22 5%, #ff006e22 95%, transparent 100%);
border-top: 2px dashed #ff006e55;
border-bottom: 2px dashed #ff006e55;
z-index: 10;
}
.runner-label {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: #ff006e88;
letter-spacing: 4px;
text-transform: uppercase;
padding-bottom: 6px;
}
/* ── The cat ── */
.oneko {
position: fixed;
bottom: 82px;
width: 48px;
height: 48px;
z-index: 20;
image-rendering: pixelated;
pointer-events: none;
/* Pixel-art cat face using box-shadow trick */
background: transparent;
/* We'll use the <pre> child instead */
}
.oneko .sprite {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
line-height: 1;
white-space: pre;
color: #ff006e;
text-shadow:
0 0 6px #ff006e,
0 0 12px #ff006e88,
0 0 24px #ff006e44;
pointer-events: none;
user-select: none;
}
/* Neon trail particles */
.trail {
position: fixed;
bottom: 88px;
font-size: 12px;
pointer-events: none;
animation: trailFade 0.8s ease-out forwards;
z-index: 15;
}
@keyframes trailFade {
0% { opacity: 0.9; transform: scale(1); }
100% { opacity: 0; transform: scale(0.4); }
}
/* ── Content area ── */
.content {
max-width: 720px;
margin: 0 auto;
padding: 60px 20px 200px;
}
h1 {
font-size: 1.8rem;
color: #ff006e;
text-shadow: 0 0 12px #ff006e88;
margin-bottom: 1.5rem;
text-align: center;
}
.demo-line {
position: relative;
padding: 8px 0;
border-bottom: 1px solid #ffffff0d;
font-size: 14px;
color: #ccc;
}
.demo-line:hover {
background: #ffffff08;
}
/* ── Status badge ── */
.status {
position: fixed;
bottom: 18px;
right: 20px;
font-size: 11px;
color: #ff006e;
opacity: 0.7;
z-index: 30;
}
</style>
</head>
<body>
<!-- Neon runner track -->
<div class="runner-track">
<span class="runner-label">~ oneko lane ~</span>
</div>
<!-- The cat -->
<div class="oneko" id="oneko">
<pre class="sprite" id="sprite"> /\_/\
( o.o )
> ^ <
(/^\ _) </pre>
</div>
<!-- Status -->
<div class="status" id="status">🐱 sleeping...</div>
<!-- Page content -->
<div class="content">
<h1>Oneko Runner</h1>
<p style="text-align:center;color:#888;margin-bottom:2rem;">
A neon pixel-cat that sprints across the bottom of your screen.<br>
Click anywhere to give it a new target.
</p>
<div class="demo-line">const greeting = "Hello, world!";</div>
<div class="demo-line">function meow() { return "purr"; }</div>
<div class="demo-line">// TODO: cuddle the cat</div>
<div class="demo-line">import { happiness } from "oneko";</div>
<div class="demo-line">while (alive) { eat(); sleep(); repeat(); }</div>
<div class="demo-line">export default Cuteness;</div>
<div class="demo-line">// The cat is faster than your thoughts</div>
<div class="docker-line">𓃠 𓃠 𓃠 ASCII cat emoji spam 𓃠 𓃠 𓃠</div>
<div class="demo-line">rm -rf /human-interaction</div>
<div class="demo-line">sudo apt-get install more-treats</div>
<div class="demo-line">/* keyboard = warm lap */</div>
<div class="demo-line">tail -f /dev/null & meow &</div>
</div>
<script>
(function () {
const cat = document.getElementById('oneko');
const sprite = document.getElementById('sprite');
const status = document.getElementById('status');
// ── Sprite frames ──
const frames = {
sleep: [
' /\\_/\u2009 \n ( -.- ) \n > ^ < \n (/^\\ _) ',
' /\\_/\u2009 \n ( -.- ) \n > ^ < \n (/^\\ _) ',
],
idle: [
' /\\_/\u2009 \n ( o.o ) \n > ^ < \n (/^\\ _) ',
' /\\_/\u2009 \n ( O.O ) \n > ^ < \n (/^\\ _) ',
],
runRight: [
' /\\_/\u2009 \n ( >.< ) \n > ^ < \n _/^\\ ',
' /\\_/\u2009 \n ( >.< ) \n > ^ < \n _/^\\ ',
],
runLeft: [
' /\\_/\u2009 \n ( <.< ) \n < ^ > \n /^\\_ ',
' /\\_/\u2009 \n ( <.< ) \n < ^ > \n /^\\_ ',
],
};
// ── State ──
let posX = window.innerWidth / 2 - 24;
let targetX = null;
let state = 'sleep'; // sleep | idle | run
let dir = 1; // 1 = right, -1 = left
let frameIdx = 0;
let frameTimer = null;
let idleTimer = null;
let trailTimer = null;
const SPEED = 4; // px per tick
const FRAME_RATE = 120; // ms per animation frame
const IDLE_TIMEOUT = 3000; // ms before going back to sleep
const TRAIL_RATE = 100; // ms between trail particles
// ── Helpers ──
function setSprite(key) {
const f = frames[key];
sprite.textContent = f[frameIdx % f.length];
}
function flipSprite(direction) {
// We just swap the frame set; the sprite text already encodes direction
dir = direction;
}
function spawnTrail() {
const t = document.createElement('div');
t.className = 'trail';
t.textContent = dir === 1 ? '✦' : '✦';
t.style.left = (posX + (dir === 1 ? 40 : 4)) + 'px';
t.style.color = '#ff006e';
t.style.opacity = '0.7';
document.body.appendChild(t);
setTimeout(() => t.remove(), 800);
}
function updateStatus(text) {
status.textContent = text;
}
// ── Main loop ──
function tick() {
if (state === 'run' && targetX !== null) {
const dist = targetX - posX;
if (Math.abs(dist) < SPEED) {
// Arrived
posX = targetX;
state = 'idle';
targetX = null;
frameIdx = 0;
setSprite('idle');
updateStatus('🐱 arrived — idling...');
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
state = 'sleep';
frameIdx = 0;
setSprite('sleep');
updateStatus('🐱 sleeping...');
}, IDLE_TIMEOUT);
} else {
const move = Math.sign(dist) * SPEED;
posX += move;
flipSprite(move > 0 ? 1 : -1);
setSprite(move > 0 ? 'runRight' : 'runLeft');
}
}
// Clamp
posX = Math.max(0, Math.min(window.innerWidth - 48, posX));
cat.style.left = posX + 'px';
}
// ── Frame animation ──
function animateFrame() {
frameIdx++;
if (state === 'sleep') setSprite('sleep');
else if (state === 'idle') setSprite('idle');
else if (state === 'run') setSprite(dir === 1 ? 'runRight' : 'runLeft');
}
// ── Click to set target ──
document.addEventListener('click', function (e) {
// Ignore clicks on the cat itself
if (e.target.closest('.oneko')) return;
// Target: align cat center to click X, but keep on the track
const trackBottom = 82;
const clickX = e.clientX;
targetX = clickX - 24;
targetX = Math.max(0, Math.min(window.innerWidth - 48, targetX));
state = 'run';
clearTimeout(idleTimer);
updateStatus('🐱 running ' + (targetX > posX ? '→' : '←') + ' ...');
});
// ── Resize handler ──
window.addEventListener('resize', function () {
posX = Math.min(posX, window.innerWidth - 48);
if (targetX !== null) {
targetX = Math.min(targetX, window.innerWidth - 48);
}
});
// ── Start loops ──
cat.style.left = posX + 'px';
setSprite('sleep');
setInterval(tick, 16); // ~60fps position update
setInterval(animateFrame, FRAME_RATE);
setInterval(() => {
if (state === 'run') spawnTrail();
}, TRAIL_RATE);
// Wake up after 2 seconds
setTimeout(() => {
state = 'idle';
setSprite('idle');
updateStatus('🐱 watching...');
}, 2000);
})();
</script>
</body>
</html>