feat: implement healthy-habits tracker MVP with animated cat

- Daily checklist of 6 health habits with click-to-toggle
- Progress bar with red/yellow/green levels and motivational phrases
- Animated cat (idle blink, hop on check, cheer at 100%, sad in evening)
- Animated forest background with time-of-day theming
- localStorage persistence with automatic midnight reset
- Confetti animation on full completion
- Responsive layout + prefers-reduced-motion support

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zhuldyz Nurpeissova 2026-05-19 12:09:02 +05:00
parent d2675e3534
commit 8a5d591ff0
8 changed files with 1022 additions and 0 deletions

140
TRD.md
View File

@ -0,0 +1,140 @@
# TRD — Дневной чеклист здоровых привычек "Здоровый день с котиком"
## 1. Описание проекта
Веб-приложение для ежедневного отслеживания привычек здорового образа жизни.
Пользователь отмечает выполненные привычки, а милый анимированный котик
реагирует на прогресс — радуется когда привычки выполняются и грустит если нет.
Всё это на фоне лёгкого анимированного лесного пейзажа.
**Настроение приложения:** уютное, мотивирующее, живое.
---
## 2. Визуальная концепция
### 2.1 Фон — лесной пейзаж
- Лёгкий анимированный фон: деревья, трава, солнечные лучи сквозь листву
- Медленно покачиваются листья и трава (CSS-анимация)
- Цветовая палитра: мягкие зелёные, бежевые, светло-голубые тона
- Время суток на фоне меняется в зависимости от реального времени:
- 🌅 Утро (6:00–12:00) — тёплый рассвет, туман
- ☀️ День (12:00–18:00) — яркий солнечный лес
- 🌙 Вечер (18:00–00:00) — закат, мягкий оранжевый свет
### 2.2 Котик — главный персонаж
- Анимированный котик сидит в углу экрана (правый нижний угол)
- Котик реагирует на действия пользователя:
- 😺 **Ждёт** — котик сидит и смотрит, иногда моргает (idle-анимация)
- 😸 **Радуется** — котик прыгает / машет хвостом когда отмечаешь привычку
- 🎉 **Ликует** — котик кружится когда выполнены все привычки
- 😿 **Грустит** — котик опускает уши если день заканчивается а привычки не выполнены
- Котик реализован через CSS-анимацию или Lottie-анимацию
- Стиль: мягкий flat illustration
---
## 3. Функции приложения (MVP)
### 3.1 Дневной чеклист привычек
Список привычек здорового образа жизни, которые нужно отметить за день:
| # | Иконка | Привычка | Описание |
|---|--------|----------|----------|
| 1 | 💧 | Вода | Выпить 8 стаканов воды (2 литра) |
| 2 | 🏃 | Движение | 30 минут ходьбы или тренировки |
| 3 | 🥗 | Здоровая еда | Съесть овощи / не есть фастфуд |
| 4 | 😴 | Сон | Лечь спать до 23:00 |
| 5 | 🧘 | Спокойствие | 10 минут медитации или чтения |
| 6 | 🚫 | Без телефона | Час без соцсетей |
### 3.2 Отметка привычки
- Нажатие на привычку отмечает её выполненной
- При отметке: маленькая анимация (галочка появляется с эффектом)
- Котик реагирует радостным прыжком
- Выполненная привычка визуально выделяется иконкой ✅
### 3.3 Прогресс дня
- Прогресс-бар показывает сколько привычек выполнено: `3 из 6`
- Цветовая индикация прогресс-бара:
- 🔴 0–2 привычки — красный
- 🟡 3–4 привычки — жёлтый
- 🟢 5–6 привычек — зелёный
- При 100% — конфетти-анимация и котик ликует 🎉
### 3.4 Мотивационная фраза
Под прогресс-баром — короткая фраза, меняется по прогрессу:
- 0%: *"Котик верит в тебя! Начни с одного шага 🐾"*
- 1–49%: *"Хорошее начало! Котик наблюдает 😺"*
- 50–99%: *"Почти всё! Не останавливайся 🔥"*
- 100%: *"Ты лучший! Котик счастлив! 🎉"*
### 3.5 Сброс в полночь
- В 00:00 чеклист автоматически сбрасывается для нового дня
- Состояние хранится в `localStorage` с датой
---
## 4. Технический стек
| Компонент | Технология |
|-----------|-----------|
| Интерфейс | HTML + CSS + Vanilla JS |
| Анимации фона | CSS keyframes / SVG-анимация |
| Анимация котика | CSS-анимация или Lottie JSON |
| Хранение данных | localStorage (без бэкенда) |
| Иконки | Emoji или SVG-иконки |
---
## 5. Структура файлов
```
project/
├── index.html # Главная страница
├── style.css # Стили и анимации фона
├── app.js # Логика чеклиста и localStorage
├── cat.css # Анимации котика
├── assets/
│ ├── cat.svg # Котик
│ ├── trees.svg # Деревья для фона
│ └── grass.svg # Трава для фона
└── README.md
```
---
## 6. Пользовательский сценарий
1. Пользователь открывает приложение утром
2. Видит лесной пейзаж и котика который моргает
3. Видит список из 6 привычек на сегодня
4. В течение дня возвращается и отмечает выполненные привычки
5. Котик радуется каждой отметке
6. Вечером видит итог — сколько привычек выполнено
7. При 100% получает анимацию праздника 🎉
8. В полночь всё сбрасывается — новый день, новые привычки
---
## 7. Что НЕ входит в MVP
- Авторизация и аккаунты
- История за прошлые дни
- Уведомления / напоминания
- Добавление своих привычек
- Мобильное приложение
---
## 8. Критерии готовности
- [ ] Отображается список из 6 привычек
- [ ] Каждую привычку можно отметить кликом
- [ ] Прогресс-бар обновляется в реальном времени
- [ ] Котик анимирован и реагирует на действия
- [ ] Фон анимирован (лес, листья, время суток)
- [ ] Данные сохраняются в localStorage
- [ ] Чеклист сбрасывается в новый день
- [ ] Приложение работает на мобильном (responsive)

259
app.js Normal file
View File

@ -0,0 +1,259 @@
(function () {
'use strict';
const HABITS = [
{ id: 'water', icon: '💧', title: 'Вода', desc: 'Выпить 8 стаканов воды (2 литра)' },
{ id: 'move', icon: '🏃', title: 'Движение', desc: '30 минут ходьбы или тренировки' },
{ id: 'food', icon: '🥗', title: 'Здоровая еда', desc: 'Съесть овощи / не есть фастфуд' },
{ id: 'sleep', icon: '😴', title: 'Сон', desc: 'Лечь спать до 23:00' },
{ id: 'calm', icon: '🧘', title: 'Спокойствие', desc: '10 минут медитации или чтения' },
{ id: 'nophone', icon: '🚫', title: 'Без телефона', desc: 'Час без соцсетей' }
];
const STORAGE_KEY = 'healthyDayWithCat';
const els = {
list: document.getElementById('habitList'),
progressText: document.getElementById('progressText'),
progressPercent: document.getElementById('progressPercent'),
progressFill: document.getElementById('progressFill'),
motivation: document.getElementById('motivation'),
date: document.getElementById('date'),
cat: document.getElementById('cat'),
confetti: document.getElementById('confetti'),
body: document.body
};
// ===== Состояние =====
function todayKey() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return freshState();
const parsed = JSON.parse(raw);
if (parsed.date !== todayKey()) return freshState();
// нормализуем — на случай если изменили список привычек
const done = {};
HABITS.forEach(h => { done[h.id] = !!parsed.done?.[h.id]; });
return { date: parsed.date, done };
} catch {
return freshState();
}
}
function freshState() {
const done = {};
HABITS.forEach(h => { done[h.id] = false; });
return { date: todayKey(), done };
}
function saveState(state) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
let state = loadState();
// ===== Рендер =====
function renderDate() {
const d = new Date();
const months = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
const weekdays = ['воскресенье','понедельник','вторник','среда','четверг','пятница','суббота'];
els.date.textContent = `${weekdays[d.getDay()]}, ${d.getDate()} ${months[d.getMonth()]}`;
}
function renderHabits() {
els.list.innerHTML = '';
HABITS.forEach(h => {
const li = document.createElement('li');
li.className = 'habit' + (state.done[h.id] ? ' done' : '');
li.dataset.id = h.id;
li.innerHTML = `
<div class="habit-icon">${h.icon}</div>
<div class="habit-text">
<div class="habit-title">${h.title}</div>
<div class="habit-desc">${h.desc}</div>
</div>
<div class="habit-check">${state.done[h.id] ? '✓' : ''}</div>
`;
li.addEventListener('click', () => toggleHabit(h.id));
els.list.appendChild(li);
});
}
function countDone() {
return HABITS.reduce((acc, h) => acc + (state.done[h.id] ? 1 : 0), 0);
}
function renderProgress() {
const done = countDone();
const total = HABITS.length;
const percent = Math.round((done / total) * 100);
els.progressText.textContent = `${done} из ${total}`;
els.progressPercent.textContent = `${percent}%`;
els.progressFill.style.width = `${percent}%`;
els.progressFill.classList.remove('level-low', 'level-mid', 'level-high');
if (done <= 2) els.progressFill.classList.add('level-low');
else if (done <= 4) els.progressFill.classList.add('level-mid');
else els.progressFill.classList.add('level-high');
// Мотивация
if (percent === 0) els.motivation.textContent = 'Котик верит в тебя! Начни с одного шага 🐾';
else if (percent < 50) els.motivation.textContent = 'Хорошее начало! Котик наблюдает 😺';
else if (percent < 100) els.motivation.textContent = 'Почти всё! Не останавливайся 🔥';
else els.motivation.textContent = 'Ты лучший! Котик счастлив! 🎉';
}
// ===== Котик =====
let happyTimer = null;
function setCatState(s) {
els.cat.dataset.state = s;
}
function reactCat(action) {
const done = countDone();
const total = HABITS.length;
if (done === total) {
setCatState('cheer');
return;
}
if (action === 'check') {
setCatState('happy');
clearTimeout(happyTimer);
happyTimer = setTimeout(() => decideIdleState(), 700);
return;
}
decideIdleState();
}
function decideIdleState() {
const done = countDone();
const total = HABITS.length;
const hour = new Date().getHours();
if (done === total) setCatState('cheer');
else if (hour >= 20 && done < total / 2) setCatState('sad'); // вечер + мало выполнено
else setCatState('idle');
}
// ===== Действия =====
function toggleHabit(id) {
state.done[id] = !state.done[id];
saveState(state);
renderHabits();
renderProgress();
if (state.done[id]) {
reactCat('check');
if (countDone() === HABITS.length) {
launchConfetti();
}
} else {
decideIdleState();
}
}
// ===== Время суток =====
function applyTimeOfDay() {
const hour = new Date().getHours();
els.body.classList.remove('morning', 'day', 'evening', 'night');
if (hour >= 6 && hour < 12) els.body.classList.add('morning');
else if (hour >= 12 && hour < 18) els.body.classList.add('day');
else if (hour >= 18 && hour < 24) els.body.classList.add('evening');
else els.body.classList.add('night');
}
// ===== Сброс в полночь / при смене даты =====
function checkDayRollover() {
if (state.date !== todayKey()) {
state = freshState();
saveState(state);
renderHabits();
renderProgress();
decideIdleState();
applyTimeOfDay();
renderDate();
} else {
applyTimeOfDay();
decideIdleState();
}
}
// ===== Конфетти =====
function launchConfetti() {
const canvas = els.confetti;
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const colors = ['#f5d76e', '#e57373', '#7fb87f', '#cfe9f5', '#c79bb8', '#ffb482'];
const particles = [];
for (let i = 0; i < 140; i++) {
particles.push({
x: Math.random() * canvas.width,
y: -20 - Math.random() * canvas.height * 0.5,
vx: (Math.random() - 0.5) * 4,
vy: 2 + Math.random() * 4,
size: 6 + Math.random() * 6,
color: colors[Math.floor(Math.random() * colors.length)],
rot: Math.random() * Math.PI * 2,
vr: (Math.random() - 0.5) * 0.3
});
}
const start = performance.now();
const duration = 3500;
function frame(now) {
const elapsed = now - start;
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.06;
p.rot += p.vr;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6);
ctx.restore();
});
if (elapsed < duration) {
requestAnimationFrame(frame);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
requestAnimationFrame(frame);
}
// ===== Init =====
renderDate();
renderHabits();
renderProgress();
applyTimeOfDay();
decideIdleState();
// проверка каждую минуту: смена дня + смена времени суток
setInterval(checkDayRollover, 60 * 1000);
// также при возврате на вкладку
document.addEventListener('visibilitychange', () => {
if (!document.hidden) checkDayRollover();
});
window.addEventListener('resize', () => {
if (els.confetti.width) {
els.confetti.width = window.innerWidth;
els.confetti.height = window.innerHeight;
}
});
})();

68
assets/cat.svg Normal file
View File

@ -0,0 +1,68 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<!-- хвост (за телом) -->
<path d="M 50 140 Q 20 130 25 95 Q 28 80 40 85"
fill="none" stroke="#d99a5b" stroke-width="14" stroke-linecap="round">
<animate attributeName="d"
dur="2.2s" repeatCount="indefinite"
values="M 50 140 Q 20 130 25 95 Q 28 80 40 85;
M 50 140 Q 18 120 30 90 Q 35 75 48 82;
M 50 140 Q 20 130 25 95 Q 28 80 40 85" />
</path>
<!-- тело -->
<ellipse cx="100" cy="140" rx="55" ry="42" fill="#f0b878" />
<!-- белое пузико -->
<ellipse cx="100" cy="155" rx="32" ry="22" fill="#fff3dc" />
<!-- лапки передние -->
<ellipse cx="78" cy="178" rx="12" ry="9" fill="#f0b878" />
<ellipse cx="122" cy="178" rx="12" ry="9" fill="#f0b878" />
<!-- голова -->
<ellipse cx="100" cy="95" rx="42" ry="38" fill="#f0b878" />
<!-- уши -->
<g class="cat-ear-l">
<polygon points="70,72 60,40 88,62" fill="#f0b878" />
<polygon points="73,68 65,48 84,62" fill="#e89a52" />
</g>
<g class="cat-ear-r">
<polygon points="130,72 140,40 112,62" fill="#f0b878" />
<polygon points="127,68 135,48 116,62" fill="#e89a52" />
</g>
<!-- мордочка светлая -->
<ellipse cx="100" cy="108" rx="22" ry="14" fill="#fff3dc" />
<!-- глаза -->
<g>
<ellipse class="cat-eye" cx="86" cy="92" rx="5" ry="7" fill="#2d2d2d" />
<circle cx="87" cy="90" r="1.5" fill="#fff" />
</g>
<g>
<ellipse class="cat-eye" cx="114" cy="92" rx="5" ry="7" fill="#2d2d2d" />
<circle cx="115" cy="90" r="1.5" fill="#fff" />
</g>
<!-- щёчки -->
<circle cx="78" cy="108" r="4" fill="#ffc2c2" opacity="0.7" />
<circle cx="122" cy="108" r="4" fill="#ffc2c2" opacity="0.7" />
<!-- носик -->
<polygon points="100,104 96,108 104,108" fill="#e07b7b" />
<!-- ротик -->
<path class="cat-mouth" d="M 92 116 Q 100 124 108 116" fill="none" stroke="#2d2d2d" stroke-width="1.8" stroke-linecap="round" />
<path d="M 100 110 L 100 114" fill="none" stroke="#2d2d2d" stroke-width="1.5" stroke-linecap="round" />
<!-- усики -->
<line x1="60" y1="108" x2="78" y2="110" stroke="#666" stroke-width="1" />
<line x1="60" y1="114" x2="78" y2="114" stroke="#666" stroke-width="1" />
<line x1="140" y1="108" x2="122" y2="110" stroke="#666" stroke-width="1" />
<line x1="140" y1="114" x2="122" y2="114" stroke="#666" stroke-width="1" />
<!-- полоски на голове -->
<path d="M 90 60 Q 92 68 90 75" stroke="#d99a5b" stroke-width="2" fill="none" />
<path d="M 100 58 Q 100 66 100 73" stroke="#d99a5b" stroke-width="2" fill="none" />
<path d="M 110 60 Q 108 68 110 75" stroke="#d99a5b" stroke-width="2" fill="none" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

39
assets/grass.svg Normal file
View File

@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 120" width="1200" height="120" preserveAspectRatio="none">
<!-- травяная полоса -->
<path d="M 0 50 Q 100 30 200 50 Q 300 70 400 45 Q 500 25 600 50 Q 700 70 800 45 Q 900 25 1000 50 Q 1100 70 1200 50 L 1200 120 L 0 120 Z" fill="#6aa66a" />
<path d="M 0 70 Q 100 55 200 75 Q 300 95 400 70 Q 500 50 600 75 Q 700 95 800 70 Q 900 50 1000 75 Q 1100 95 1200 75 L 1200 120 L 0 120 Z" fill="#558e55" />
<!-- травинки -->
<g stroke="#558e55" stroke-width="2" fill="none" stroke-linecap="round">
<path d="M 50 60 Q 52 45 55 38" />
<path d="M 60 60 Q 58 50 56 44" />
<path d="M 130 55 Q 132 38 136 30" />
<path d="M 140 55 Q 138 45 134 38" />
<path d="M 250 62 Q 248 48 244 40" />
<path d="M 260 60 Q 264 45 270 38" />
<path d="M 380 58 Q 378 42 374 34" />
<path d="M 390 58 Q 394 45 400 38" />
<path d="M 510 60 Q 508 45 504 38" />
<path d="M 520 60 Q 524 45 528 38" />
<path d="M 640 62 Q 638 48 634 40" />
<path d="M 650 60 Q 654 45 660 38" />
<path d="M 770 58 Q 768 42 764 34" />
<path d="M 780 58 Q 784 45 790 38" />
<path d="M 900 60 Q 898 45 894 38" />
<path d="M 910 60 Q 914 45 918 38" />
<path d="M 1030 58 Q 1028 45 1024 38" />
<path d="M 1040 60 Q 1044 45 1048 38" />
<path d="M 1150 62 Q 1148 48 1144 40" />
<path d="M 1160 60 Q 1164 45 1168 38" />
</g>
<!-- цветочки -->
<g>
<circle cx="180" cy="65" r="4" fill="#f5d76e" /><circle cx="180" cy="65" r="1.5" fill="#fff" />
<circle cx="320" cy="70" r="4" fill="#e57373" /><circle cx="320" cy="70" r="1.5" fill="#fff" />
<circle cx="560" cy="65" r="4" fill="#fff" /><circle cx="560" cy="65" r="1.5" fill="#f5d76e" />
<circle cx="720" cy="72" r="4" fill="#f5d76e" /><circle cx="720" cy="72" r="1.5" fill="#fff" />
<circle cx="880" cy="65" r="4" fill="#c79bb8" /><circle cx="880" cy="65" r="1.5" fill="#fff" />
<circle cx="1080" cy="70" r="4" fill="#f5d76e" /><circle cx="1080" cy="70" r="1.5" fill="#fff" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

47
assets/trees.svg Normal file
View File

@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 400" width="1200" height="400" preserveAspectRatio="xMidYEnd meet">
<!-- дерево 1 -->
<g>
<rect x="90" y="280" width="20" height="120" fill="#7a5230" />
<ellipse cx="100" cy="240" rx="80" ry="90" fill="#7fb87f" />
<ellipse cx="65" cy="260" rx="40" ry="50" fill="#6ea96e" />
<ellipse cx="135" cy="265" rx="42" ry="48" fill="#8ec48e" />
</g>
<!-- дерево 2 -->
<g>
<rect x="280" y="300" width="16" height="100" fill="#7a5230" />
<ellipse cx="288" cy="260" rx="60" ry="70" fill="#88c388" />
<ellipse cx="260" cy="275" rx="35" ry="40" fill="#78b378" />
</g>
<!-- дерево 3 (центр большое) -->
<g>
<rect x="490" y="260" width="26" height="140" fill="#6e4824" />
<ellipse cx="503" cy="210" rx="100" ry="115" fill="#7fb87f" />
<ellipse cx="450" cy="240" rx="50" ry="65" fill="#6ea96e" />
<ellipse cx="560" cy="240" rx="55" ry="60" fill="#92c992" />
</g>
<!-- дерево 4 -->
<g>
<rect x="730" y="290" width="18" height="110" fill="#7a5230" />
<ellipse cx="739" cy="250" rx="70" ry="80" fill="#8ec48e" />
<ellipse cx="710" cy="265" rx="38" ry="45" fill="#7fb87f" />
</g>
<!-- дерево 5 -->
<g>
<rect x="930" y="280" width="20" height="120" fill="#6e4824" />
<ellipse cx="940" cy="235" rx="85" ry="95" fill="#7fb87f" />
<ellipse cx="985" cy="260" rx="42" ry="50" fill="#92c992" />
<ellipse cx="900" cy="265" rx="38" ry="42" fill="#6ea96e" />
</g>
<!-- дерево 6 -->
<g>
<rect x="1110" y="295" width="14" height="105" fill="#7a5230" />
<ellipse cx="1117" cy="260" rx="55" ry="65" fill="#88c388" />
</g>
<!-- кустики -->
<ellipse cx="220" cy="370" rx="50" ry="22" fill="#6ea96e" />
<ellipse cx="400" cy="375" rx="40" ry="18" fill="#7fb87f" />
<ellipse cx="620" cy="370" rx="55" ry="22" fill="#6ea96e" />
<ellipse cx="820" cy="375" rx="45" ry="20" fill="#7fb87f" />
<ellipse cx="1030" cy="370" rx="50" ry="22" fill="#6ea96e" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

88
cat.css Normal file
View File

@ -0,0 +1,88 @@
.cat-container {
position: fixed;
right: 20px;
bottom: 12px;
z-index: 10;
pointer-events: none;
}
.cat {
width: 160px;
height: 160px;
position: relative;
transform-origin: bottom center;
animation: cat-breathe 3.5s ease-in-out infinite;
}
.cat-img {
width: 100%;
height: 100%;
display: block;
user-select: none;
-webkit-user-drag: none;
}
@keyframes cat-breathe {
0%, 100% { transform: scaleY(1) scaleX(1); }
50% { transform: scaleY(1.02) scaleX(0.99); }
}
/* idle — лёгкое моргание (через моргание глаз в SVG) */
.cat[data-state="idle"] .cat-eye {
animation: blink 4.5s infinite;
transform-origin: center;
}
@keyframes blink {
0%, 92%, 100% { transform: scaleY(1); }
94%, 98% { transform: scaleY(0.1); }
}
/* happy — короткий прыжок */
.cat[data-state="happy"] {
animation: cat-hop 0.6s cubic-bezier(0.22, 1.61, 0.36, 1);
}
@keyframes cat-hop {
0% { transform: translateY(0) rotate(0); }
30% { transform: translateY(-30px) rotate(-4deg); }
60% { transform: translateY(-10px) rotate(3deg); }
100% { transform: translateY(0) rotate(0); }
}
/* cheer — крутится */
.cat[data-state="cheer"] {
animation: cat-cheer 1.2s ease-in-out infinite;
}
@keyframes cat-cheer {
0% { transform: translateY(0) rotate(0); }
25% { transform: translateY(-25px) rotate(180deg); }
50% { transform: translateY(0) rotate(360deg); }
75% { transform: translateY(-15px) rotate(540deg); }
100% { transform: translateY(0) rotate(720deg); }
}
/* sad — уши опускаются (через класс), котик слегка приседает */
.cat[data-state="sad"] {
animation: cat-sad 4s ease-in-out infinite;
filter: saturate(0.7) brightness(0.95);
}
.cat[data-state="sad"] .cat-ear-l { transform: rotate(35deg) translate(-2px, 4px); transform-origin: bottom right; }
.cat[data-state="sad"] .cat-ear-r { transform: rotate(-35deg) translate(2px, 4px); transform-origin: bottom left; }
.cat[data-state="sad"] .cat-mouth { d: path("M 92 132 Q 100 124 108 132"); }
@keyframes cat-sad {
0%, 100% { transform: translateY(0) scaleY(1); }
50% { transform: translateY(2px) scaleY(0.97); }
}
@media (max-width: 600px) {
.cat { width: 120px; height: 120px; }
.cat-container { right: 10px; bottom: 8px; }
}
@media (prefers-reduced-motion: reduce) {
.cat, .cat[data-state] { animation: none; }
}

56
index.html Normal file
View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Здоровый день с котиком</title>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="cat.css" />
</head>
<body>
<div class="scene" id="scene">
<div class="sky"></div>
<div class="sun"></div>
<div class="rays"></div>
<div class="trees-layer">
<img src="assets/trees.svg" alt="" class="trees trees-back" />
<img src="assets/trees.svg" alt="" class="trees trees-front" />
</div>
<div class="grass-layer">
<img src="assets/grass.svg" alt="" class="grass" />
</div>
</div>
<main class="app">
<header class="app-header">
<h1>Здоровый день с котиком 🐾</h1>
<p class="date" id="date"></p>
</header>
<section class="progress-section">
<div class="progress-info">
<span id="progressText">0 из 6</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<p class="motivation" id="motivation"></p>
</section>
<section class="habits">
<ul id="habitList" class="habit-list"></ul>
</section>
</main>
<div class="cat-container">
<div class="cat" id="cat" data-state="idle">
<img src="assets/cat.svg" alt="Котик" class="cat-img" />
</div>
</div>
<canvas id="confetti" class="confetti"></canvas>
<script src="app.js"></script>
</body>
</html>

325
style.css Normal file
View File

@ -0,0 +1,325 @@
:root {
--green-soft: #a8d5a8;
--green-deep: #6aa66a;
--beige: #f5e9d4;
--sky-blue: #cfe9f5;
--sun-yellow: #ffe28a;
--text: #3b4a3b;
--card: rgba(255, 255, 255, 0.78);
--card-border: rgba(106, 166, 106, 0.35);
--progress-red: #e57373;
--progress-yellow: #f4c95d;
--progress-green: #6aa66a;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
color: var(--text);
overflow-x: hidden;
}
body {
min-height: 100vh;
position: relative;
background: linear-gradient(180deg, var(--sky-blue) 0%, #eaf6e3 100%);
}
/* ===== Сцена / фон ===== */
.scene {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
transition: filter 1s ease;
}
.sky {
position: absolute;
inset: 0;
background: linear-gradient(180deg, var(--sky-blue) 0%, #eaf6e3 70%, #d8ecbf 100%);
transition: background 1s ease;
}
.sun {
position: absolute;
top: 8%;
right: 12%;
width: 110px;
height: 110px;
border-radius: 50%;
background: radial-gradient(circle, #fff5c8 0%, var(--sun-yellow) 50%, rgba(255, 226, 138, 0) 75%);
filter: blur(2px);
animation: sun-pulse 6s ease-in-out infinite;
transition: background 1s ease, top 1s ease, right 1s ease;
}
@keyframes sun-pulse {
0%, 100% { transform: scale(1); opacity: 0.9; }
50% { transform: scale(1.08); opacity: 1; }
}
.rays {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
115deg,
rgba(255, 255, 200, 0) 0px,
rgba(255, 255, 200, 0) 60px,
rgba(255, 255, 200, 0.18) 62px,
rgba(255, 255, 200, 0) 80px
);
pointer-events: none;
animation: rays-shift 14s linear infinite;
}
@keyframes rays-shift {
from { transform: translateX(0); }
to { transform: translateX(-80px); }
}
.trees-layer {
position: absolute;
bottom: 80px;
left: 0;
right: 0;
height: 50vh;
pointer-events: none;
}
.trees {
position: absolute;
bottom: 0;
width: 110%;
left: -5%;
opacity: 0.95;
transform-origin: bottom center;
animation: tree-sway 7s ease-in-out infinite;
}
.trees-back {
opacity: 0.55;
filter: blur(1px) brightness(0.95);
transform: translateY(20px) scale(0.92);
animation-duration: 9s;
animation-delay: -2s;
}
@keyframes tree-sway {
0%, 100% { transform: rotate(-0.4deg); }
50% { transform: rotate(0.4deg); }
}
.grass-layer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 120px;
pointer-events: none;
}
.grass {
position: absolute;
bottom: 0;
width: 110%;
left: -5%;
animation: grass-wave 4s ease-in-out infinite;
transform-origin: bottom center;
}
@keyframes grass-wave {
0%, 100% { transform: skewX(-1deg); }
50% { transform: skewX(1deg); }
}
/* ===== Время суток ===== */
body.morning .sky { background: linear-gradient(180deg, #ffd8b8 0%, #ffe7c8 50%, #e8e0c0 100%); }
body.morning .sun { background: radial-gradient(circle, #fff0c8 0%, #ffb482 60%, rgba(255,180,130,0) 80%); top: 25%; }
body.day .sky { background: linear-gradient(180deg, var(--sky-blue) 0%, #eaf6e3 70%, #d8ecbf 100%); }
body.evening .sky { background: linear-gradient(180deg, #f5b27a 0%, #f4a6a6 40%, #c79bb8 100%); }
body.evening .sun { background: radial-gradient(circle, #ffe2a8 0%, #ff8a55 60%, rgba(255,138,85,0) 80%); top: 55%; right: 8%; }
body.evening .scene { filter: saturate(0.95); }
body.night .sky { background: linear-gradient(180deg, #2c2a4a 0%, #4b4373 60%, #6e5a86 100%); }
body.night .sun { display: none; }
body.night .scene { filter: brightness(0.8); }
/* ===== Приложение ===== */
.app {
max-width: 560px;
margin: 0 auto;
padding: 32px 20px 220px;
position: relative;
z-index: 1;
}
.app-header {
text-align: center;
margin-bottom: 24px;
}
.app-header h1 {
margin: 0 0 6px;
font-size: 28px;
color: #2d3e2d;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
}
.date {
margin: 0;
font-size: 14px;
color: #5c6e5c;
opacity: 0.85;
}
.progress-section {
background: var(--card);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 18px 20px;
margin-bottom: 18px;
box-shadow: 0 6px 18px rgba(60, 90, 60, 0.1);
}
.progress-info {
display: flex;
justify-content: space-between;
font-weight: 600;
margin-bottom: 10px;
font-size: 16px;
}
.progress-bar {
width: 100%;
height: 14px;
background: rgba(180, 200, 180, 0.4);
border-radius: 999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
background: var(--progress-red);
border-radius: 999px;
transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1), background 0.4s ease;
}
.progress-fill.level-low { background: var(--progress-red); }
.progress-fill.level-mid { background: var(--progress-yellow); }
.progress-fill.level-high { background: var(--progress-green); }
.motivation {
margin: 12px 0 0;
font-style: italic;
text-align: center;
font-size: 15px;
color: #44563f;
}
.habit-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.habit {
background: var(--card);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--card-border);
border-radius: 14px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 14px;
cursor: pointer;
user-select: none;
transition: transform 0.15s ease, background 0.25s ease, border-color 0.25s ease;
box-shadow: 0 4px 12px rgba(60, 90, 60, 0.08);
}
.habit:hover { transform: translateY(-1px); }
.habit:active { transform: translateY(0) scale(0.99); }
.habit.done {
background: rgba(190, 230, 190, 0.85);
border-color: rgba(106, 166, 106, 0.7);
}
.habit.done .habit-title {
text-decoration: line-through;
opacity: 0.7;
}
.habit-icon {
font-size: 28px;
width: 40px;
text-align: center;
flex-shrink: 0;
}
.habit-text { flex: 1; }
.habit-title { font-weight: 600; font-size: 16px; }
.habit-desc { font-size: 13px; opacity: 0.78; margin-top: 2px; }
.habit-check {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid var(--green-deep);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.6);
color: var(--green-deep);
font-weight: 800;
font-size: 16px;
}
.habit.done .habit-check {
background: var(--green-deep);
color: #fff;
animation: check-pop 0.4s cubic-bezier(0.22, 1.61, 0.36, 1);
}
@keyframes check-pop {
0% { transform: scale(0.4); }
60% { transform: scale(1.25); }
100% { transform: scale(1); }
}
/* ===== Конфетти ===== */
.confetti {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 50;
}
/* ===== Адаптив ===== */
@media (max-width: 600px) {
.app { padding: 20px 14px 220px; }
.app-header h1 { font-size: 22px; }
.sun { width: 80px; height: 80px; top: 6%; right: 8%; }
.trees-layer { height: 35vh; bottom: 60px; }
.grass-layer { height: 80px; }
}
@media (prefers-reduced-motion: reduce) {
.sun, .rays, .trees, .grass { animation: none; }
.progress-fill { transition: width 0.2s linear, background 0.2s linear; }
}