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:
parent
d2675e3534
commit
8a5d591ff0
140
TRD.md
140
TRD.md
@ -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
259
app.js
Normal 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
68
assets/cat.svg
Normal 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
39
assets/grass.svg
Normal 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
47
assets/trees.svg
Normal 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
88
cat.css
Normal 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
56
index.html
Normal 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
325
style.css
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user