Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>moonlight.exe</title>
<style>
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
min-height: 100vh;
background: radial-gradient(circle at 50% 40%, #101024 0%, #050510 70%, #000 100%);
display: flex; align-items: center; justify-content: center;
font-family: "Courier New", monospace;
overflow: hidden;
}
.window {
position: relative;
width: 960px;
max-width: 95vw;
background: #f0ead6;
border: 2px solid #0a0a1a;
box-shadow:
8px 8px 0 rgba(240, 234, 214, 0.12),
0 30px 80px rgba(240, 234, 214, 0.08),
0 20px 60px rgba(0,0,0,0.7);
transition: opacity 0.5s ease, transform 0.5s ease, width 0.4s ease;
transform-origin: center;
}
.window.closed { opacity: 0; transform: scale(0.85); pointer-events: none; }
.window.maximized { width: min(100vw, calc((100vh - 40px) * 16 / 9)); max-width: none; }
.titlebar {
display: flex; align-items: center; justify-content: space-between;
background: #f0ead6; height: 30px; padding: 0 8px;
border-bottom: 2px solid #0a0a1a; user-select: none;
}
.titlebar .icon { width: 16px; height: 16px; background: #0a0a1a; }
.titlebar .title {
font-size: 12px; color: #0a0a1a; font-weight: bold;
letter-spacing: 2px; text-transform: lowercase;
}
.titlebar .buttons { display: flex; gap: 6px; }
.btn {
width: 18px; height: 18px;
border: 2px solid #0a0a1a; background: transparent;
position: relative; cursor: pointer; padding: 0; outline: none;
transition: background 0.12s;
}
.btn:hover { background: rgba(10,10,26,0.12); }
.btn:active { background: rgba(10,10,26,0.28); }
.btn.min::after {
content: ""; position: absolute;
left: 3px; right: 3px; bottom: 4px;
height: 2px; background: #0a0a1a;
}
.btn.max::after {
content: ""; position: absolute;
left: 4px; right: 4px; top: 4px; bottom: 4px;
border: 2px solid #0a0a1a;
}
.btn.close::before,
.btn.close::after {
content: ""; position: absolute;
left: 50%; top: 50%;
width: 2px; height: 13px; background: #0a0a1a;
transform-origin: center;
}
.btn.close::before { transform: translate(-50%, -50%) rotate(45deg); }
.btn.close::after { transform: translate(-50%, -50%) rotate(-45deg); }
.scene {
position: relative; width: 100%;
aspect-ratio: 16 / 9;
background: #0a0a1a;
overflow: hidden;
max-height: 1500px;
transition: max-height 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.window.minimized .scene { max-height: 0; }
canvas {
display: block; width: 100%; height: 100%;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.restore {
position: fixed; bottom: 40px; left: 50%;
transform: translateX(-50%) translateY(20px);
background: #f0ead6; color: #0a0a1a;
border: 2px solid #0a0a1a;
padding: 10px 22px;
font-family: inherit; font-weight: bold;
cursor: pointer; letter-spacing: 2px; font-size: 12px;
opacity: 0; pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
box-shadow: 4px 4px 0 rgba(240, 234, 214, 0.2);
}
.restore.visible { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
.restore:hover { background: #fff; }
</style>
</head>
<body>
<div class="window" id="win">
<div class="titlebar">
<div class="icon"></div>
<div class="title">moonlight</div>
<div class="buttons">
<button class="btn min" id="btnMin" title="Свернуть"></button>
<button class="btn max" id="btnMax" title="Развернуть"></button>
<button class="btn close" id="btnClose" title="Закрыть"></button>
</div>
</div>
<div class="scene">
<canvas id="c"></canvas>
</div>
</div>
<button class="restore" id="btnRestore">ОТКРЫТЬ</button>
<script>
/* =====================================================
CANVAS
===================================================== */
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const W = 480, H = 270;
canvas.width = W;
canvas.height = H;
ctx.imageSmoothingEnabled = false;
const CREAM = '#f0ead6';
const NIGHT = 'rgb(10,10,26)';
/* =====================================================
RANDOM
===================================================== */
let seed = 1337;
function rnd() {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
}
/* =====================================================
BACKGROUND (static)
===================================================== */
const bgCanvas = document.createElement('canvas');
bgCanvas.width = W; bgCanvas.height = H;
const bg = bgCanvas.getContext('2d');
bg.imageSmoothingEnabled = false;
// Функция для «пиксельного» круга
function fillCircle(c, cx, cy, r, color) {
c.fillStyle = color;
for (let y = -r - 1; y <= r + 1; y++) {
for (let x = -r - 1; x <= r + 1; x++) {
const d = x*x + y*y;
if (d <= r*r + r*0.55) c.fillRect(cx + x, cy + y, 1, 1);
}
}
}
// Полумесяц (разница двух кругов)
function drawCrescent(c, cx, cy, R, cutX, cutY, rCut) {
c.fillStyle = CREAM;
for (let y = -R - 2; y <= R + 2; y++) {
for (let x = -R - 2; x <= R + 2; x++) {
const inOuter = (x*x + y*y) <= R*R + R*0.55;
const dx = x - cutX, dy = y - cutY;
const inCut = (dx*dx + dy*dy) <= rCut*rCut + rCut*0.55;
if (inOuter && !inCut) c.fillRect(cx + x, cy + y, 1, 1);
}
}
}
const MOON_X = 240, MOON_Y = 88, MOON_R = 34;
function buildBackground() {
// Градиент неба
for (let y = 0; y < H; y++) {
const t = Math.min(1, y / (H - 55));
const r = Math.round(8 + (28 - 8) * t);
const g = Math.round(8 + (28 - 8) * t);
const b = Math.round(26 + (54 - 26) * t);
bg.fillStyle = `rgb(${r},${g},${b})`;
bg.fillRect(0, y, W, 1);
}
// Мягкое свечение вокруг луны
const glow = bg.createRadialGradient(MOON_X, MOON_Y, MOON_R * 0.8, MOON_X, MOON_Y, MOON_R * 4);
glow.addColorStop(0, 'rgba(240,234,214,0.13)');
glow.addColorStop(0.5, 'rgba(240,234,214,0.05)');
glow.addColorStop(1, 'rgba(240,234,214,0)');
bg.fillStyle = glow;
bg.fillRect(MOON_X - MOON_R*4, MOON_Y - MOON_R*4, MOON_R*8, MOON_R*8);
// Луна
drawCrescent(bg, MOON_X, MOON_Y, MOON_R, 20, -3, MOON_R - 2);
// Кратеры на освещённой части
bg.fillStyle = 'rgba(10,10,26,0.30)';
bg.fillRect(MOON_X - 22, MOON_Y - 12, 4, 4);
bg.fillRect(MOON_X - 14, MOON_Y + 6, 3, 3);
bg.fillRect(MOON_X - 24, MOON_Y + 4, 3, 3);
bg.fillRect(MOON_X - 10, MOON_Y - 20, 3, 3);
bg.fillRect(MOON_X - 20, MOON_Y - 2, 2, 2);
bg.fillRect(MOON_X - 6, MOON_Y + 14, 2, 2);
// Дальние холмы (силуэт)
bg.fillStyle = 'rgba(240,234,214,0.07)';
for (let x = 0; x < W; x++) {
const y = H - 48 - Math.sin(x * 0.014) * 9 - Math.sin(x * 0.037 + 1) * 5;
bg.fillRect(x, Math.floor(y), 1, H - Math.floor(y));
}
bg.fillStyle = 'rgba(240,234,214,0.10)';
for (let x = 0; x < W; x++) {
const y = H - 38 - Math.sin(x * 0.02 + 2) * 6;
bg.fillRect(x, Math.floor(y), 1, H - Math.floor(y));
}
// Ближний холм — «земля»
bg.fillStyle = CREAM;
for (let x = 0; x < W; x++) {
const y = H - 28 - Math.sin(x * 0.021 + 0.5) * 5 - Math.sin(x * 0.05) * 3;
bg.fillRect(x, Math.floor(y), 1, H - Math.floor(y));
}
// Тёмный контур
bg.fillStyle = 'rgba(10,10,26,0.5)';
for (let x = 0; x < W; x++) {
const y = H - 28 - Math.sin(x * 0.021 + 0.5) * 5 - Math.sin(x * 0.05) * 3;
bg.fillRect(x, Math.floor(y), 1, 1);
}
// Травинки
seed = 777;
bg.fillStyle = CREAM;
for (let x = 6; x < W; x += 6 + Math.floor(rnd() * 14)) {
if (Math.abs(x - W/2) < 32) continue; // не под котом
const y = Math.floor(H - 28 - Math.sin(x * 0.021 + 0.5) * 5 - Math.sin(x * 0.05) * 3);
const h = 1 + Math.floor(rnd() * 2);
bg.fillRect(x, y - h, 1, h);
if (rnd() < 0.55) bg.fillRect(x - 1, y - h + 1, 1, 1);
if (rnd() < 0.55) bg.fillRect(x + 1, y - h + 1, 1, 1);
if (rnd() < 0.25) bg.fillRect(x, y - h - 1, 1, 1);
}
}
/* =====================================================
STARS
===================================================== */
const stars = [];
function buildStars() {
seed = 987654;
for (let i = 0; i < 460; i++) {
const x = Math.floor(rnd() * W);
const y = Math.floor(rnd() * (H - 70));
const type = rnd();
const phase = rnd() * Math.PI * 2;
const speed = 0.4 + rnd() * 3.2;
const bright = 0.45 + rnd() * 0.55;
stars.push({ x, y, type, phase, speed, bright });
}
}
function drawStars(t) {
for (let i = 0; i < stars.length; i++) {
const s = stars[i];
const twinkle = Math.sin(s.phase + t * s.speed) * 0.5 + 0.5;
const alpha = s.bright * (0.25 + twinkle * 0.75);
ctx.globalAlpha = alpha;
ctx.fillStyle = CREAM;
if (s.type < 0.07) {
// Плюсик + мерцающий центр
ctx.fillRect(s.x, s.y - 1, 1, 3);
ctx.fillRect(s.x - 1, s.y, 3, 1);
if (twinkle > 0.6) {
ctx.globalAlpha = alpha * 0.6;
ctx.fillRect(s.x - 2, s.y, 1, 1);
ctx.fillRect(s.x + 2, s.y, 1, 1);
ctx.fillRect(s.x, s.y - 2, 1, 1);
ctx.fillRect(s.x, s.y + 2, 1, 1);
}
} else if (s.type < 0.18) {
// Крупный квадратик
ctx.fillRect(s.x, s.y, 2, 2);
if (twinkle > 0.75) {
ctx.globalAlpha = alpha * 0.5;
ctx.fillRect(s.x - 1, s.y, 1, 1);
ctx.fillRect(s.x + 2, s.y, 1, 1);
ctx.fillRect(s.x, s.y - 1, 1, 1);
ctx.fillRect(s.x, s.y + 2, 1, 1);
}
} else {
// Точка
ctx.fillRect(s.x, s.y, 1, 1);
}
}
ctx.globalAlpha = 1;
}
/* =====================================================
SHOOTING STARS
===================================================== */
const shooters = [];
let shootTimer = 1.5;
function updateShooters(dt) {
shootTimer -= dt;
if (shootTimer <= 0) {
shootTimer = 1.8 + Math.random() * 3.5;
const fromLeft = Math.random() < 0.5;
shooters.push({
x: fromLeft ? -10 : W + 10,
y: 20 + Math.random() * (H * 0.35),
vx: (fromLeft ? 1 : -1) * (140 + Math.random() * 90),
vy: 35 + Math.random() * 45,
life: 1.6 + Math.random() * 1.2,
trail: []
});
}
for (let i = shooters.length - 1; i >= 0; i--) {
const s = shooters[i];
s.x += s.vx * dt;
s.y += s.vy * dt;
s.trail.push({ x: s.x, y: s.y });
if (s.trail.length > 18) s.trail.shift();
s.life -= dt;
if (s.life <= 0 || s.x < -60 || s.x > W + 60 || s.y > H) {
shooters.splice(i, 1);
}
}
}
function drawShooters() {
ctx.fillStyle = CREAM;
for (const s of shooters) {
const n = s.trail.length;
for (let i = 0; i < n; i++) {
const p = s.trail[i];
const t = i / n;
ctx.globalAlpha = Math.pow(t, 1.6);
const sz = i === n - 1 ? 2 : 1;
ctx.fillRect(Math.floor(p.x), Math.floor(p.y), sz, sz);
}
}
ctx.globalAlpha = 1;
}
/* =====================================================
MOON DRIPS
===================================================== */
const drips = [];
function initDrips() {
const basePositions = [
{ x: MOON_X - 18, y: MOON_Y + MOON_R - 6 },
{ x: MOON_X - 12, y: MOON_Y + MOON_R - 3 },
{ x: MOON_X - 6, y: MOON_Y + MOON_R - 1 },
];
basePositions.forEach((p, i) => {
drips.push({
x: p.x, startY: p.y, y: p.y,
vy: 0,
maxDrop: 5 + Math.random() * 10,
delay: i * 600 + Math.random() * 900,
});
});
}
function updateDrips(dt) {
for (const d of drips) {
if (d.delay > 0) { d.delay -= dt * 1000; continue; }
d.vy += dt * 22;
d.y += d.vy * dt * 40;
if (d.y - d.startY > d.maxDrop) {
d.y = d.startY; d.vy = 0;
d.delay = 900 + Math.random() * 1800;
d.maxDrop = 4 + Math.random() * 10;
}
}
}
function drawDrips() {
ctx.fillStyle = CREAM;
for (const d of drips) {
if (d.delay > 0) continue;
ctx.fillRect(Math.floor(d.x), Math.floor(d.y), 1, 2);
if (d.vy > 30) ctx.fillRect(Math.floor(d.x), Math.floor(d.y) + 2, 1, 1);
}
}
/* =====================================================
CAT
===================================================== */
function drawCat(t) {
const cx = Math.floor(W / 2);
const groundY = Math.floor(H - 28 - Math.sin(cx * 0.021 + 0.5) * 5 - Math.sin(cx * 0.05) * 3);
const by = groundY;
ctx.fillStyle = CREAM;
/* ---- ХВОСТ (за телом) ---- */
// Хвост — цепочка толстых пикселей по кривой с бегущей волной
const segs = 16;
for (let i = 0; i <= segs; i++) {
const k = i / segs;
// Форма: уходит вправо-вверх и изгибается назад к коту
const baseX = cx + 8 + k * 16;
const baseY = by - 4 - k * 16 + Math.pow(k, 2) * 6;
// Волна — сила растёт к концу хвоста
const wave = Math.sin(t * 6 - k * 5.5) * (2 + k * 5);
const x = baseX + wave * 0.55;
const y = baseY + wave;
const size = i < 3 ? 4 : i < 9 ? 3 : 2;
ctx.fillRect(Math.floor(x - size / 2), Math.floor(y - size / 2), size, size);
}
/* ---- ТЕЛО ---- */
const bodyTop = by - 20;
const bodyH = 20;
for (let y = 0; y < bodyH; y++) {
const k = y / (bodyH - 1);
const halfW = 5 + Math.floor(k * 4);
ctx.fillRect(cx - halfW, bodyTop + y, halfW * 2 + 1, 1);
}
/* ---- ГОЛОВА ---- */
const headCY = bodyTop - 7;
const headR = 8;
fillCircle(ctx, cx, headCY, headR, CREAM);
/* ---- УШКИ ---- */
for (let i = 0; i < 6; i++) {
const w = 6 - Math.floor(i * 0.8);
ctx.fillRect(cx - 8 + Math.floor(i * 0.3), headCY - headR - 1 - i, w, 1);
ctx.fillRect(cx + 8 - Math.floor(i * 0.3) - w + 1, headCY - headR - 1 - i, w, 1);
}
/* ---- МОРДА (вырезы) ---- */
ctx.fillStyle = NIGHT;
// Медленное моргание раз в ~5 секунд
const blinkCycle = (t * 0.22) % 5;
const blinking = blinkCycle > 4.85;
if (!blinking) {
// Глазки-точки
ctx.fillRect(cx - 4, headCY - 1, 2, 3);
ctx.fillRect(cx + 3, headCY - 1, 2, 3);
// Блик
ctx.fillStyle = CREAM;
ctx.fillRect(cx - 4, headCY - 1, 1, 1);
ctx.fillRect(cx + 3, headCY - 1, 1, 1);
ctx.fillStyle = NIGHT;
} else {
// Закрытые глазки
ctx.fillRect(cx - 4, headCY + 1, 2, 1);
ctx.fillRect(cx + 3, headCY + 1, 2, 1);
}
// Носик
ctx.fillRect(cx - 1, headCY + 3, 2, 2);
// Ротик
ctx.fillRect(cx - 2, headCY + 5, 1, 1);
ctx.fillRect(cx, headCY + 5, 1, 1);
ctx.fillRect(cx - 1, headCY + 5, 1, 1);
}
/* =====================================================
MAIN LOOP
===================================================== */
let lastT = 0;
let elapsed = 0;
let running = true;
function loop(now) {
if (!running) return;
const dt = Math.min(0.05, (now - lastT) / 1000) || 0;
lastT = now;
elapsed += dt;
// Фон
ctx.drawImage(bgCanvas, 0, 0);
// Звёзды
drawStars(elapsed);
// Капли с луны
updateDrips(dt);
drawDrips();
// Падающие звёзды
updateShooters(dt);
drawShooters();
// Кот
drawCat(elapsed);
requestAnimationFrame(loop);
}
/* =====================================================
INIT
===================================================== */
buildBackground();
buildStars();
initDrips();
requestAnimationFrame(t => { lastT = t; loop(t); });
/* =====================================================
WINDOW CONTROLS
===================================================== */
const win = document.getElementById('win');
const btnMin = document.getElementById('btnMin');
const btnMax = document.getElementById('btnMax');
const btnClose = document.getElementById('btnClose');
const btnRestore = document.getElementById('btnRestore');
btnMin.addEventListener('click', () => win.classList.toggle('minimized'));
btnMax.addEventListener('click', () => win.classList.toggle('maximized'));
btnClose.addEventListener('click', () => {
win.classList.add('closed');
btnRestore.classList.add('visible');
});
btnRestore.addEventListener('click', () => {
win.classList.remove('closed');
btnRestore.classList.remove('visible');
});
// Esc — закрыть окно
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
win.classList.add('closed');
btnRestore.classList.add('visible');
}
});
</script>
</body>
</html>