Интерактивная поздравительная открытка на JavaScript
1. Введение
1.1 Идея проекта — эмоции в цифровом формате
Наверное, каждый из нас ловил себя на мысли: что отправить на день рождения в этот раз? Просто текст, тёплую фотографию или голосовое сообщение? И сразу вспоминается это чувство, когда ищешь или обдумываешь креативный текст, а потом вспоминаешь о милой картинке с котиком, гифке с шампанским или стандартном «С ДР!» — и отправляешь, чисто для галочки.
Со временем я заметил: когда получаешь такое поздравление в Telegram, становится немного грустно, что человек хоть и поздравил, но не потратил время на то, чтобы обдумать и искренне пожелать чего-то хорошего и уникального только про тебя. Хотя понимаешь — это нормально. Поколение постарше привыкло обмениваться этими милыми, но безликими посланиями. Хотя не все делают это из-за нежелания — некоторым просто понравилась картинка, вот и скинули.
А недавно за чаем друг рассказал историю о том, как в 10-м классе поздравил одноклассницу с днём рождения. Он решил, что написать в сообщение или купить бумажную открытку — слишком просто. Попытался сделать что-то своими руками, но не получилось (если говорить кратко). Тогда он создал простую электронную открытку, где при тряске экрана падало конфетти. «Ты открываешь на телефоне — трясёшь, и бах!» — смеялся он, вспоминая. Девочке открытка понравилась именно потому, что была сделана специально для неё, своими руками. Если подумать — она не выглядела профессионально или красиво, но эта ручная работа, сделанная с душой, заставила её восхититься.
После этой беседы меня осенило: почему бы не сделать нечто подобное сейчас? Ведь такая открытка до сих пор остаётся необычной и уникальной — она не стала обыденной вещью. Не искать в Яндексе очередную картинку, а создать интерактивную историю, с которой можно взаимодействовать. Чтобы человек не просто пролистал фотографию, а почувствовал: это сделано именно для него.
Вот как выглядит сам проект: интерактивная открытка, в которой каждый элемент реагирует на действия пользователя. Сначала идёт конверт — не просто картинка, а элемент, который можно «взять» и открыть. Затем появляется торт со свечой. Свечу можно зажечь слайдером — переключаешь, и она загорается. А если нажать на сам торт — выскакивает случайное пожелание и запускается один из семи эффектов: летящие сердца, фейерверки и др. Конечно, в будущем можно придумать что-то более уникальное, но для первой версии подойдёт — главное, передать идею. Может, кто-то из вас, читающих эту статью, подскажет, что лучше добавить вместо этих эффектов.
Всё строится на чистом HTML, CSS и JavaScript — без всяких сложностей. Я считаю, чем проще, тем больше людей смогут его понять и посмотреть. В проекте важно сохранить ауру Handmade, чтобы поздравление воспринималось как эксклюзив.
Это пока только прототип — основа, которую можно персонализировать, но уже сейчас я вижу, что в мире, где все обмениваются однотипными картинками, можно подарить уникальный, сделанный вручную (хоть и в коде) открытку.
В итоге мы же поздравляем не для вида. Мы хотим сказать: «Ты занимаешь важное место в моих мыслях, и это время, потраченное на поиск нужных слов, — лишь малая часть той теплоты, которую я к тебе чувствую». И иногда для этого действительно достаточно просто отправить котика, но иногда даже созданного маленького мира не хватает, чтобы выразить всё, что хочешь сказать человеку.
1.2 Подготовка к разработке
Для создания проекта не требовалось сложных инструментов. Весь код заключён в трёх основных технологиях, которые прекрасно работают в любом современном браузере:
Технологический стек:
HTML — семантическая разметка и структура
CSS — визуальное оформление, анимации, адаптивный дизайн
JavaScript — интерактивность и логика работы
Структура проекта:
birthday-card/
├── index.html # Основная структура открытки
├── styles.css # Все стили и анимации
└── script.js # Вся интерактивная логикаПрелесть этого проекта в простоте — его можно легко назвать чистым кодом. Открытка работает в любом браузере: от настольного компьютера до мобильного телефона.
2. Основная часть
2.1 Архитектура взаимодействия
Цель проекта — создать цифровой аналог реальных действий, которые ассоциируются с праздником. Дать почувствовать, будто пользователь держит в руках не пиксели, а настоящую открытку, бережно сделанную вручную. Каждый элемент открытки был сделан так, чтобы были приятные ощущения.
Основные части проекта:
Конверт — традиционный элемент любого поздравления, который нужно «открыть».
Свеча на торте — символ праздника, который обычно задувают, но прежде нужно зажечь.
Сам торт — главный элемент, реагирующий на прикосновения.
Каждая часть была реализована с учётом реального опыта поздравления — того, как мы обычно поздравляем вживую. Например, анимация конверта использует принцип предвкушения: сначала пользователь видит красивый конверт и только когда его открывает, видит содержимое.
2.2 Конверт: первый контакт и создание интриги
Конверт — это входная точка. Он создан не просто как статичный элемент, а как полноценный интерактивный элемент, который подготавливает пользователя к праздничному настроению, т. к. без интриги нет никакого ощущения волшебства.
Архитектура конверта:
class BirthdayCard {
constructor() {
this.isEnvelopeOpened = false;
this.elements = {
envelope: document.getElementById('envelope'),
envelopeScreen: document.getElementById('envelopeScreen')
};
}
init() {
document.addEventListener('DOMContentLoaded', () => {
this.initEnvelope();
});
}
initEnvelope() {
this.elements.envelope.addEventListener('click', () => this.openEnvelope());
setInterval(() => {
if (!this.isEnvelopeOpened && Math.random() < 0.2) {
this.createEnvelopeSparkle();
}
}, 1000);
}
openEnvelope() {
if (this.isEnvelopeOpened) return;
this.isEnvelopeOpened = true;
this.elements.envelope.classList.add('opening');
this.createFlashEffect();
this.createEnvelopeConfetti();
setTimeout(() => {
this.elements.envelopeScreen.classList.add('hidden');
document.body.classList.add('card-visible');
document.getElementById('cardContent').classList.add('visible');
}, 800);
}
createEnvelopeSparkle() {
const color = this.getRandomColor(['#FF4081', '#7C4DFF', '#40C4FF', '#FFD700']);
const particle = document.createElement('div');
particle.className = 'envelope-sparkle';
Object.assign(particle.style, {
background: color,
width: '10px',
height: '10px',
left: `${Math.random() * window.innerWidth}px`,
top: `${Math.random() * window.innerHeight}px`,
boxShadow: `0 0 20px ${color}`,
borderRadius: '50%',
position: 'fixed',
pointerEvents: 'none',
zIndex: '10000'
});
document.body.appendChild(particle);
particle.animate([
{ transform: 'scale(0)', opacity: 0 },
{ transform: 'scale(1.3)', opacity: 1 },
{ transform: 'scale(0)', opacity: 0 }
], {
duration: 1500 + Math.random() * 800,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
}).onfinish = () => particle.remove();
}
createFlashEffect() {
const flash = document.createElement('div');
flash.className = 'flash-effect';
Object.assign(flash.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: 'rgba(255, 255, 255, 0.95)',
zIndex: '9999',
opacity: '0',
pointerEvents: 'none'
});
document.body.appendChild(flash);
flash.animate([
{ opacity: 0 },
{ opacity: 1 },
{ opacity: 0 }
], {
duration: 500,
easing: 'ease-out'
}).onfinish = () => flash.remove();
}
createEnvelopeConfetti() {
for (let i = 0; i < 40; i++) {
setTimeout(() => {
const color = this.getRandomColor(['#FF4081', '#7C4DFF', '#40C4FF', '#FFD700', '#FF9800']);
const angle = Math.random() * Math.PI * 2;
const distance = 100 + Math.random() * 150;
const particle = document.createElement('div');
particle.className = 'envelope-confetti';
Object.assign(particle.style, {
background: color,
width: '15px',
height: '15px',
left: '50%',
top: '50%',
position: 'fixed',
pointerEvents: 'none',
zIndex: '10000'
});
document.body.appendChild(particle);
particle.animate([
{
transform: 'translate(-50%, -50%) scale(1) rotate(0deg)',
opacity: 1
},
{
transform: `translate(calc(-50% + ${Math.cos(angle) * distance}px), calc(-50% + ${Math.sin(angle) * distance}px)) scale(0) rotate(${Math.random() * 720}deg)`,
opacity: 0
}
], {
duration: 1000 + Math.random() * 800,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
}).onfinish = () => particle.remove();
}, i * 15);
}
}
getRandomColor(colorArray) {
return colorArray[Math.floor(Math.random() * colorArray.length)];
}
}
const birthdayCard = new BirthdayCard();
birthdayCard.init();Ключевые особенности реализации конверта:
Само открытие — это каскад хорошо поставленных жестов. Клик запускает цепочку: конверт совершает изящный 3D-поворот (CSS-класс opening), будто его поднимают и переворачивают в руках. Мгновенная белая вспышка (createFlashEffect) озаряет всё вокруг, создавая ощущение чуда (или ощущение, прям как от флешки в CS2 от друга). И в этот самый момент из центра конверта вырывается пучок праздничных конфетти — сорок разноцветных частиц, летящих во все стороны с естественной, «бумажной» траекторией (createEnvelopeConfetti). Лишь после этой короткой, но насыщенной анимации, спустя ровно 800 миллисекунд, экран плавно затемняется, чтобы открыть главное содержимое. Всё это — чистый JavaScript и CSS без единой сторонней библиотеки, чтобы магия оставалась лёгкой и быстрой.
2.3 Свеча и система управления: создание магии
Свеча здесь — это главный элемент праздника. Мне хотелось связать её с одной из легенд, почему люди начали ставить свечи в праздничный торт: греки зажигали свечу на пироге и верили, что пламя свечи и её дым помогают донести молитвы до богини Артемиды. В моём проекте этот древний жест превращается в слайдер — он зажигает свечку, и происходит анимация огня, которая активирует дополнительные эффекты, словно исполняя маленькое желание.
Архитектура системы свечи:
class BirthdayCard {
constructor() {
this.isCandleLit = false;
this.elements = {
flame: document.getElementById('flame'),
candleSlider: document.getElementById('candleSlider'),
candleValue: document.getElementById('candleValue'),
interactiveCake: document.getElementById('interactiveCake')
};
}
initCandleSlider() {
this.elements.candleSlider.addEventListener('input', () => {
const value = parseInt(this.elements.candleSlider.value);
this.isCandleLit = value === 1;
this.elements.flame.classList.toggle('lit', this.isCandleLit);
this.elements.candleValue.textContent = this.isCandleLit ? "Вкл" : "Выкл";
if (this.isCandleLit) {
this.createSparklesAroundCake();
this.showMessage("✨ Загадай свое желание! ✨", 3000);
}
});
}
createSparklesAroundCake() {
const rect = this.elements.interactiveCake.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2 - 150;
for (let i = 0; i < 15; i++) {
setTimeout(() => {
const angle = Math.random() * Math.PI * 2;
const distance = 20 + Math.random() * 35;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
this.createSparkle(x, y, '#FFD700', 12, 800);
}, i * 50);
}
}
createSparkle(x, y, color = '#FFD700', size = 12, duration = 600) {
const particle = document.createElement('div');
particle.className = 'particle sparkle-dot';
Object.assign(particle.style, {
background: color,
width: `${size}px`,
height: `${size}px`,
left: `${x}px`,
top: `${y}px`,
boxShadow: `0 0 ${size * 2}px ${color}`,
borderRadius: '50%',
position: 'fixed',
pointerEvents: 'none',
zIndex: '10000'
});
document.body.appendChild(particle);
particle.animate([
{ transform: 'scale(1) rotate(0deg)', opacity: 1 },
{ transform: 'scale(1.8) rotate(180deg)', opacity: 0.9 },
{ transform: 'scale(0) rotate(360deg)', opacity: 0 }
], {
duration,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
}).onfinish = () => particle.remove();
}
showMessage(text, duration = 5000) {
const message = document.createElement('div');
message.className = 'message-popup';
message.textContent = text;
document.body.appendChild(message);
message.animate([
{ top: '-100px', opacity: 0, transform: 'translateX(-50%) scale(0.8)' },
{ top: '20px', opacity: 1, transform: 'translateX(-50%) scale(1)' }
], {
duration: 600,
easing: 'ease-out',
fill: 'forwards'
});
setTimeout(() => {
message.animate([
{ top: '20px', opacity: 1, transform: 'translateX(-50%) scale(1)' },
{ top: '-100px', opacity: 0, transform: 'translateX(-50%) scale(0.8)' }
], {
duration: 600,
easing: 'ease-in'
}).onfinish = () => message.remove();
}, duration);
}
}Но магия не ограничивается свечой. Этот жест как задувание спички — рождает последствия. Вокруг торта мгновенно вспыхивает ореол из пятнадцати золотистых искр (createSparklesAroundCake), летящих по мягкой сферической траектории. А следом, словно эхо загаданного желания, в центре экрана появляется тёплое, персонализированное сообщение-подсказка (showMessage), которое тактично исчезает через несколько секунд.
2.4 Торт и система эффектов: ядро праздничной магии
Торт — это самый интерактивный элемент открытки, который реагирует на каждое прикосновение: визуальных эффектов, анимаций и сообщений. Его реализация представляет собой сложную систему взаимодействия различных подсистем. Также в том куске главное — это система эффектов, от тонких блёсток до масштабных фейерверков — создаёт многослойную праздничную атмосферу.
Архитектура торта и системы эффектов:
class BirthdayCard {
constructor() {
this.elements = {
interactiveCake: document.getElementById('interactiveCake')
};
this.effects = ['confetti', 'hearts', 'sparkles', 'fireworks', 'rainbow', 'spirals', 'lightning'];
this.wishes = [
"🍀 Пусть удача будет твоей верной спутницей!",
"💖 Любви, которая согревает сердце каждый день!",
"💰 Финансового благополучия и стабильности!",
"🎁 Побед во всех начинаниях и достижения целей!",
// ... остальные пожелания
];
}
initCakeInteraction() {
this.elements.interactiveCake.addEventListener('click', (e) => {
e.stopPropagation();
this.animateCakeClick();
this.createCakeSparkles(e);
const randomEffect = this.getRandomEffect();
const randomWish = this.getRandomWish();
this.activateEffect(randomEffect);
this.showMessage(randomWish, 5000);
});
}
animateCakeClick() {
this.elements.interactiveCake.style.transform = 'scale(1.1)';
setTimeout(() => {
this.elements.interactiveCake.style.transform = '';
}, 250);
}
createCakeSparkles(event) {
for (let i = 0; i < 12; i++) {
setTimeout(() => {
this.createSparkle(event.clientX, event.clientY, '#FFD700', 10, 600);
}, i * 30);
}
}
activateEffect(effect, count = null) {
const effects = {
confetti: () => this.createConfettiRain(count || 40),
fireworks: () => this.createFireworks(count || 5),
hearts: () => this.createHeartExplosion(count || 35),
sparkles: () => this.createSparkleStorm(count || 60),
spirals: () => this.createSpiralEffect(count || 5),
rainbow: () => this.createRainbowEffect(count || 6),
lightning: () => this.createLightningEffect(count || 6)
};
if (effects[effect]) {
effects[effect]();
}
}
createConfettiRain(count) {
for (let i = 0; i < count; i++) {
setTimeout(() => {
const color = this.getRandomColor(['#FF4081', '#7C4DFF', '#40C4FF', '#FFD700', '#FF9800', '#76FF03', '#FF6F00']);
const size = Math.random() * 15 + 8;
const x = Math.random() * window.innerWidth;
const endX = (Math.random() - 0.5) * 200;
this.createParticle({
className: 'confetti-piece',
color,
size,
x,
y: -40,
duration: 1500 + Math.random() * 1000,
animation: [
{ transform: 'translateY(0) rotate(0deg)', opacity: 1 },
{ transform: `translate(${endX}px, 120vh) rotate(${Math.random() * 1080}deg)`, opacity: 0 }
]
});
}, i * 15);
}
}
createHeartExplosion(count) {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const heartTypes = ['❤️', '💖', '💕', '💗', '💓', '💘', '💝'];
for (let i = 0; i < count; i++) {
setTimeout(() => {
const heart = document.createElement('div');
heart.className = 'particle heart-particle';
heart.innerHTML = heartTypes[i % heartTypes.length];
const color = ['#FF4081', '#E91E63', '#C2185B', '#FF5252'][i % 4];
const angle = Math.random() * Math.PI * 2;
const distance = 100 + Math.random() * 150;
Object.assign(heart.style, {
color,
fontSize: `${2.2 + Math.random() * 1.5}em`,
position: 'fixed',
left: `${centerX}px`,
top: `${centerY}px`,
zIndex: '10000',
pointerEvents: 'none',
transform: 'translate(-50%, -50%) scale(0)'
});
document.body.appendChild(heart);
heart.animate([
{ transform: 'translate(-50%, -50%) scale(0) rotate(0deg)', opacity: 0 },
{ transform: 'translate(-50%, -50%) scale(1.8) rotate(0deg)', opacity: 1, offset: 0.15 },
{ transform: `translate(calc(-50% + ${Math.cos(angle) * distance}px), calc(-50% + ${Math.sin(angle) * distance}px)) scale(0.8) rotate(${Math.random() * 720}deg)`, opacity: 0 }
], {
duration: 1800 + Math.random() * 800,
easing: 'cubic-bezier(0.2, 0.8, 0.4, 1)'
}).onfinish = () => heart.remove();
}, i * 30);
}
}
getRandomEffect() {
return this.effects[Math.floor(Math.random() * this.effects.length)];
}
getRandomWish() {
return this.wishes[Math.floor(Math.random() * this.wishes.length)];
}
createParticle(options) {
const { className, color, size, x, y, duration, animation } = options;
const particle = document.createElement('div');
particle.className = `particle ${className}`;
Object.assign(particle.style, {
background: color,
width: `${size}px`,
height: `${size}px`,
left: `${x}px`,
top: `${y}px`,
boxShadow: `0 0 ${size * 2}px ${color}`,
borderRadius: '50%',
position: 'fixed',
pointerEvents: 'none',
zIndex: '10000'
});
document.body.appendChild(particle);
particle.animate(animation, {
duration,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
}).onfinish = () => particle.remove();
return particle;
}
}Ключевые принципы реализации:
Система делает глубокий вдох и запускает одну из семи масштабных анимаций, выбранную случайным образом для сохранения эффекта неожиданности. От роя конфетти до взрыва алых сердец, от фейерверка до радужной спирали — каждый эффект представляет собой сложную систему частиц, рождённых универсальной и оптимизированной фабрикой createParticle. Эта система умна: она не грузит устройство, адаптируя количество и интенсивность частиц, и каждое шоу завершается уникальным тёплым пожеланием, выбранным из десяти заранее заготовленных фраз.
Весь этот визуальный спектакль анимирован не просто плавно, а физически правдоподобно. Web Animations API в паре с кинематографичными кривыми Безье задаёт частицам естественные траектории — с ускорением, дуговым полётом и мягким затуханием. Это превращает холодный рендеринг в тёплое, почти осязаемое праздничное настроение, где каждый клик — это не вызов функции, а создание маленького, запоминающегося момента.
Весь код, кому интересно.
JS
class BirthdayCard {
constructor() {
this.isCandleLit = false;
this.isEnvelopeOpened = false;
this.elements = {
envelopeScreen: document.getElementById('envelopeScreen'),
envelope: document.getElementById('envelope'),
cardContent: document.getElementById('cardContent'),
flame: document.getElementById('flame'),
interactiveCake: document.getElementById('interactiveCake'),
candleSlider: document.getElementById('candleSlider'),
candleValue: document.getElementById('candleValue'),
candle: document.querySelector('.candle')
};
this.colors = {
confetti: ["#FF4081", "#7C4DFF", "#40C4FF", "#FFD700", "#FF9800", "#76FF03", "#FF6F00"],
hearts: ["#FF4081", "#E91E63", "#C2185B", "#FF5252"],
sparkles: ["#FFD700", "#FFEB3B", "#FFF59D", "#FFF176"],
fireworks: ["#FF4081", "#7C4DFF", "#40C4FF", "#FFD700", "#76FF03"],
rainbow: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"],
spirals: ["#FF4081", "#7C4DFF", "#40C4FF", "#00BCD4", "#4CAF50"],
lightning: ["#FFFF00", "#FFEB3B", "#FFF176", "#FFD700"]
};
this.wishes = [
"🍀 Пусть удача будет твоей верной спутницей!",
"💖 Любви, которая согревает сердце каждый день!",
"💰 Финансового благополучия и стабильности!",
"🎁 Побед во всех начинаниях и достижения целей!",
"💖 Волшебных моментов и незабываемых впечатлений!",
"🎁 Ярких идей и вдохновения для новых проектов!",
"💖 Стремительного роста и профессиональных успехов!",
"🎁 Точности в решениях и мудрости в выборе!",
"💖 Радуги эмоций и солнечного настроения!",
"🎁 Приятных сюрпризов и неожиданных радостей!"
];
this.effects = ['confetti', 'hearts', 'sparkles', 'fireworks', 'rainbow', 'spirals', 'lightning'];
this.init();
}
init() {
document.addEventListener('DOMContentLoaded', () => {
this.adjustCandlePosition();
this.initEnvelope();
});
}
adjustCandlePosition() {
if (this.elements.candle) {
const currentBottom = parseInt(window.getComputedStyle(this.elements.candle).bottom) || 0;
this.elements.candle.style.bottom = `${currentBottom + 50}px`;
}
}
initEnvelope() {
this.elements.envelope.addEventListener('click', () => this.openEnvelope());
setInterval(() => {
if (!this.isEnvelopeOpened && Math.random() < 0.2) {
this.createEnvelopeSparkle();
}
}, 1000);
}
openEnvelope() {
if (this.isEnvelopeOpened) return;
this.isEnvelopeOpened = true;
this.elements.envelope.classList.add('opening');
this.createFlashEffect();
this.createEnvelopeConfetti();
setTimeout(() => {
this.transitionToCard();
}, 800);
}
transitionToCard() {
this.elements.envelopeScreen.classList.add('hidden');
document.body.classList.add('card-visible');
this.elements.cardContent.classList.add('visible');
this.initCandleSlider();
this.initCakeInteraction();
setTimeout(() => {
this.showMessage("🎉 С Днём Рождения, Роман!");
}, 500);
setTimeout(() => {
this.activateEffect('confetti', 25);
}, 1000);
}
initCandleSlider() {
this.elements.candleSlider.addEventListener('input', () => {
const value = parseInt(this.elements.candleSlider.value);
this.isCandleLit = value === 1;
this.elements.flame.classList.toggle('lit', this.isCandleLit);
this.elements.candleValue.textContent = this.isCandleLit ? "Вкл" : "Выкл";
if (this.isCandleLit) {
this.createSparklesAroundCake();
this.showMessage("✨ Загадай свое желание! ✨", 3000);
}
});
}
initCakeInteraction() {
this.elements.interactiveCake.addEventListener('click', (e) => {
e.stopPropagation();
this.animateCakeClick();
this.createCakeSparkles(e);
const randomEffect = this.getRandomEffect();
const randomWish = this.getRandomWish();
this.activateEffect(randomEffect);
this.showMessage(randomWish, 5000);
});
}
createParticle(options) {
const {
type = 'particle',
className = '',
color = '#FFD700',
size = 12,
x = 0,
y = 0,
duration = 600,
animation = [],
onRemove = () => {}
} = options;
const particle = document.createElement('div');
particle.className = `particle ${type} ${className}`;
Object.assign(particle.style, {
background: color,
width: `${size}px`,
height: `${size}px`,
left: `${x}px`,
top: `${y}px`,
boxShadow: `0 0 ${size * 2}px ${color}`,
borderRadius: '50%',
position: 'fixed',
pointerEvents: 'none',
zIndex: '10000'
});
document.body.appendChild(particle);
const anim = particle.animate(animation, {
duration,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
});
anim.onfinish = () => {
particle.remove();
onRemove();
};
return particle;
}
createSparkle(x, y, color = '#FFD700', size = 12, duration = 600) {
return this.createParticle({
type: 'sparkle-dot',
color,
size,
x,
y,
duration,
animation: [
{ transform: 'scale(1) rotate(0deg)', opacity: 1 },
{ transform: 'scale(1.8) rotate(180deg)', opacity: 0.9 },
{ transform: 'scale(0) rotate(360deg)', opacity: 0 }
]
});
}
createEnvelopeSparkle() {
const color = this.getRandomColor(['#FF4081', '#7C4DFF', '#40C4FF', '#FFD700']);
this.createParticle({
className: 'envelope-sparkle',
color,
size: 10,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
duration: 1500 + Math.random() * 800,
animation: [
{ transform: 'scale(0)', opacity: 0 },
{ transform: 'scale(1.3)', opacity: 1 },
{ transform: 'scale(0)', opacity: 0 }
]
});
}
createEnvelopeConfetti() {
for (let i = 0; i < 40; i++) {
setTimeout(() => {
const color = this.getRandomColor(['#FF4081', '#7C4DFF', '#40C4FF', '#FFD700', '#FF9800']);
const angle = Math.random() * Math.PI * 2;
const distance = 100 + Math.random() * 150;
this.createParticle({
className: 'envelope-confetti',
color,
size: 15,
x: '50%',
y: '50%',
duration: 1000 + Math.random() * 800,
animation: [
{
transform: 'translate(-50%, -50%) scale(1) rotate(0deg)',
opacity: 1
},
{
transform: `translate(calc(-50% + ${Math.cos(angle) * distance}px), calc(-50% + ${Math.sin(angle) * distance}px)) scale(0) rotate(${Math.random() * 720}deg)`,
opacity: 0
}
]
});
}, i * 15);
}
}
createFlashEffect() {
const flash = document.createElement('div');
flash.className = 'flash-effect';
Object.assign(flash.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: 'rgba(255, 255, 255, 0.95)',
zIndex: '9999',
opacity: '0',
pointerEvents: 'none'
});
document.body.appendChild(flash);
flash.animate([
{ opacity: 0 },
{ opacity: 1 },
{ opacity: 0 }
], {
duration: 500,
easing: 'ease-out'
}).onfinish = () => flash.remove();
}
activateEffect(effect, count = null) {
const effects = {
confetti: () => this.createConfettiRain(count || 40),
fireworks: () => this.createFireworks(count || 5),
hearts: () => this.createHeartExplosion(count || 35),
sparkles: () => this.createSparkleStorm(count || 60),
spirals: () => this.createSpiralEffect(count || 5),
rainbow: () => this.createRainbowEffect(count || 6),
lightning: () => this.createLightningEffect(count || 6)
};
if (effects[effect]) {
effects[effect]();
}
}
createConfettiRain(count) {
for (let i = 0; i < count; i++) {
setTimeout(() => {
const color = this.getRandomColor(this.colors.confetti);
const size = Math.random() * 15 + 8;
const x = Math.random() * window.innerWidth;
const endX = (Math.random() - 0.5) * 200;
this.createParticle({
className: 'confetti-piece',
color,
size,
x,
y: -40,
duration: 1500 + Math.random() * 1000,
animation: [
{
transform: 'translateY(0) rotate(0deg)',
opacity: 1
},
{
transform: `translate(${endX}px, 120vh) rotate(${Math.random() * 1080}deg)`,
opacity: 0
}
]
});
}, i * 15);
}
}
createFireworks(count) {
for (let i = 0; i < count; i++) {
setTimeout(() => {
const x = Math.random() * window.innerWidth;
const y = 100 + Math.random() * window.innerHeight * 0.6;
const color = this.getRandomColor(this.colors.fireworks);
this.createSparkle(x, y, color, 45, 500);
setTimeout(() => {
for (let j = 0; j < 20; j++) {
setTimeout(() => {
const angle = Math.random() * Math.PI * 2;
const distance = 30 + Math.random() * 80;
this.createParticle({
className: 'firework-dot',
color,
size: 6,
x,
y,
duration: 800 + Math.random() * 600,
animation: [
{
transform: 'translate(0, 0) scale(1)',
opacity: 1
},
{
transform: `translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px) scale(0)`,
opacity: 0
}
]
});
}, j * 10);
}
}, 100);
}, i * 250);
}
}
createHeartExplosion(count) {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const heartTypes = ['❤️', '💖', '💕', '💗', '💓', '💘', '💝'];
for (let i = 0; i < count; i++) {
setTimeout(() => {
const heart = document.createElement('div');
heart.className = 'particle heart-particle';
heart.innerHTML = heartTypes[i % heartTypes.length];
const color = this.colors.hearts[i % this.colors.hearts.length];
const size = 2.2 + Math.random() * 1.5;
const angle = Math.random() * Math.PI * 2;
const distance = 100 + Math.random() * 150;
const rotation = Math.random() * 720;
const scale = 0.7 + Math.random() * 0.6;
Object.assign(heart.style, {
color,
fontSize: `${size}em`,
textShadow: `0 0 20px ${color}, 0 0 40px rgba(255, 255, 255, 0.8)`,
position: 'fixed',
left: `${centerX}px`,
top: `${centerY}px`,
zIndex: '10000',
pointerEvents: 'none',
transform: 'translate(-50%, -50%) scale(0)'
});
document.body.appendChild(heart);
heart.animate([
{
transform: 'translate(-50%, -50%) scale(0) rotate(0deg)',
opacity: 0
},
{
transform: 'translate(-50%, -50%) scale(1.8) rotate(0deg)',
opacity: 1,
offset: 0.15
},
{
transform: `translate(calc(-50% + ${Math.cos(angle) * distance}px), calc(-50% + ${Math.sin(angle) * distance}px)) scale(${scale}) rotate(${rotation}deg)`,
opacity: 0
}
], {
duration: 1800 + Math.random() * 800,
easing: 'cubic-bezier(0.2, 0.8, 0.4, 1)'
}).onfinish = () => heart.remove();
}, i * 30);
}
}
createSparkleStorm(count) {
for (let i = 0; i < count; i++) {
setTimeout(() => {
const color = this.getRandomColor(this.colors.sparkles);
const size = Math.random() * 8 + 4;
const x = Math.random() * window.innerWidth;
this.createParticle({
className: 'sparkle-dot',
color,
size,
x,
y: -40,
duration: 1200 + Math.random() * 800,
animation: [
{
transform: 'translateY(0) scale(1)',
opacity: 1
},
{
transform: 'translateY(110vh) scale(0.3)',
opacity: 0
}
]
});
}, i * 20);
}
}
createSpiralEffect(count) {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
for (let i = 0; i < count; i++) {
const spiralCount = 24;
const color = this.getRandomColor(this.colors.spirals);
for (let j = 0; j < spiralCount; j++) {
setTimeout(() => {
const angle = (j / spiralCount) * Math.PI * 2;
const distance = 100;
const turns = 2.5;
const keyframes = [];
for (let k = 0; k <= 15; k++) {
const progress = k / 15;
const currentAngle = angle + progress * Math.PI * 2 * turns;
const currentDistance = progress * distance;
const x = Math.cos(currentAngle) * currentDistance;
const y = Math.sin(currentAngle) * currentDistance;
keyframes.push({
transform: `translate(${x}px, ${y}px)`,
opacity: 1 - progress
});
}
this.createParticle({
className: 'spiral-particle',
color,
size: 9,
x: centerX,
y: centerY,
duration: 1800,
animation: keyframes
});
}, i * 150 + j * 10);
}
}
}
createRainbowEffect(count) {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
for (let i = 0; i < count; i++) {
this.colors.rainbow.forEach((color, colorIndex) => {
for (let j = 0; j < 9; j++) {
setTimeout(() => {
const angle = Math.random() * Math.PI * 2;
const distance = 80 + Math.random() * 120;
this.createParticle({
className: 'rainbow-particle',
color,
size: 14,
x: centerX,
y: centerY,
duration: 1100 + Math.random() * 700,
animation: [
{
transform: 'translate(0, 0) scale(1)',
opacity: 1
},
{
transform: `translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px) scale(0)`,
opacity: 0
}
]
});
}, i * 60 + colorIndex * 20 + j * 3);
}
});
}
}
createLightningEffect(count) {
for (let i = 0; i < count; i++) {
setTimeout(() => {
const startX = Math.random() * window.innerWidth;
const endX = startX + (Math.random() - 0.5) * 200;
const color = this.getRandomColor(this.colors.lightning);
for (let j = 0; j < 6; j++) {
setTimeout(() => {
const segmentX = startX + (j/6) * (endX - startX) + (Math.random() - 0.5) * 35;
const segmentY = (j/6) * window.innerHeight * 0.9;
this.createParticle({
className: 'lightning-particle',
color,
width: 8,
height: 35,
x: segmentX,
y: segmentY,
duration: 200 + Math.random() * 150,
animation: [
{ opacity: 0, filter: 'brightness(1)' },
{ opacity: 1, filter: 'brightness(4)' },
{ opacity: 0.7, filter: 'brightness(2)' },
{ opacity: 1, filter: 'brightness(5)' },
{ opacity: 0, filter: 'brightness(1)' }
],
iterations: 2
});
}, j * 25);
}
}, i * 350);
}
}
animateCakeClick() {
this.elements.interactiveCake.style.transform = 'scale(1.1)';
setTimeout(() => {
this.elements.interactiveCake.style.transform = '';
}, 250);
}
createCakeSparkles(event) {
for (let i = 0; i < 12; i++) {
setTimeout(() => {
this.createSparkle(event.clientX, event.clientY, '#FFD700', 10, 600);
}, i * 30);
}
}
createSparklesAroundCake() {
const rect = this.elements.interactiveCake.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2 - 150;
for (let i = 0; i < 15; i++) {
setTimeout(() => {
const angle = Math.random() * Math.PI * 2;
const distance = 20 + Math.random() * 35;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
this.createSparkle(x, y, '#FFD700', 12, 800);
}, i * 50);
}
}
showMessage(text, duration = 5000) {
const oldMessage = document.querySelector('.message-popup');
if (oldMessage) oldMessage.remove();
const message = document.createElement('div');
message.className = 'message-popup';
message.textContent = text;
document.body.appendChild(message);
message.style.left = '50%';
message.style.transform = 'translateX(-50%)';
message.style.textAlign = 'center';
message.animate([
{ top: '-100px', opacity: 0, transform: 'translateX(-50%) scale(0.8)' },
{ top: '20px', opacity: 1, transform: 'translateX(-50%) scale(1)' }
], {
duration: 600,
easing: 'ease-out',
fill: 'forwards'
});
setTimeout(() => {
message.animate([
{ top: '20px', opacity: 1, transform: 'translateX(-50%) scale(1)' },
{ top: '-100px', opacity: 0, transform: 'translateX(-50%) scale(0.8)' }
], {
duration: 600,
easing: 'ease-in'
}).onfinish = () => message.remove();
}, duration);
}
getRandomColor(colorArray) {
return colorArray[Math.floor(Math.random() * colorArray.length)];
}
getRandomEffect() {
return this.effects[Math.floor(Math.random() * this.effects.length)];
}
getRandomWish() {
return this.wishes[Math.floor(Math.random() * this.wishes.length)];
}
}
const birthdayCard = new BirthdayCard();CSS
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-user-select: none;
user-select: none;
}
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
overflow-x: hidden;
font-family: 'Times New Roman', Times, serif;
position: relative;
cursor: default;
transition: background 1s ease;
}
body.card-visible {
background: linear-gradient(135deg, #ffccd5 0%, #b5e6ff 100%);
overflow-y: auto;
}
.envelope-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
z-index: 10000;
transition: opacity 0.8s ease;
}
.envelope-screen.hidden {
opacity: 0;
pointer-events: none;
}
.envelope-container {
position: relative;
perspective: 1000px;
width: 100%;
max-width: 500px;
padding: 20px;
}
.envelope {
position: relative;
width: 320px;
height: 220px;
margin: 0 auto;
cursor: pointer;
transform-style: preserve-3d;
transition: transform 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.envelope.opening {
transform: rotateY(180deg) scale(0.9);
}
.envelope-front, .envelope-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 10px;
overflow: hidden;
}
.envelope-front {
background: linear-gradient(135deg, #ff4081, #7C4DFF);
transform-style: preserve-3d;
box-shadow:
0 20px 50px rgba(255, 64, 129, 0.4),
0 10px 30px rgba(124, 77, 255, 0.3),
inset 0 -2px 10px rgba(0, 0, 0, 0.2);
}
.envelope-flap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 40%;
background: linear-gradient(135deg, #ff4081, #ff6b9d);
clip-path: polygon(0 0, 50% 100%, 100% 0);
transform-origin: top center;
transition: transform 0.5s ease;
z-index: 2;
}
.envelope.opening .envelope-flap {
transform: rotateX(-180deg);
}
.envelope-body {
position: absolute;
top: 40%;
left: 0;
width: 100%;
height: 60%;
background: linear-gradient(135deg, #7C4DFF, #40C4FF);
border-radius: 0 0 10px 10px;
padding: 20px;
}
.envelope-design {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
bottom: 10px;
border: 2px dashed rgba(255, 255, 255, 0.3);
border-radius: 5px;
pointer-events: none;
}
.stamp {
position: absolute;
top: 15px;
right: 15px;
width: 60px;
height: 60px;
background: #FFD700;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4);
border: 2px solid white;
}
.stamp-text {
font-size: 24px;
}
.to-label {
position: absolute;
bottom: 20px;
left: 20px;
color: white;
font-family: 'Times New Roman', Times, serif;
}
.label-text {
font-size: 16px;
opacity: 0.9;
margin-bottom: 5px;
font-weight: bold;
}
.name-highlight {
font-size: 32px;
font-weight: bold;
text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.4);
color: #FFD700;
}
.envelope-back {
background: linear-gradient(135deg, #40C4FF, #7C4DFF);
transform: rotateY(180deg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow:
0 20px 50px rgba(64, 196, 255, 0.4),
0 10px 30px rgba(124, 77, 255, 0.3);
}
.envelope-message {
text-align: center;
color: white;
padding: 20px;
}
.message-icon {
font-size: 45px;
margin-bottom: 10px;
}
.message-text {
font-size: 20px;
font-weight: bold;
margin-bottom: 5px;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3);
}
.message-hint {
font-size: 28px;
}
.open-instruction {
margin-top: 40px;
text-align: center;
color: rgba(255, 255, 255, 0.9);
}
.instruction-text {
font-size: 18px;
margin-bottom: 10px;
font-weight: bold;
}
.card-content-wrapper {
opacity: 0;
transform: scale(0.95);
transition: opacity 0.8s ease, transform 0.8s ease;
min-height: 100vh;
padding: 20px;
position: relative;
z-index: 1;
}
.card-content-wrapper.visible {
opacity: 1;
transform: scale(1);
}
.container {
max-width: 1200px;
margin: 0 auto;
position: relative;
z-index: 2;
}
.card {
background: rgba(255, 255, 255, 0.97);
border-radius: 35px;
padding: 45px;
margin: 20px auto;
box-shadow:
0 25px 70px rgba(255, 64, 129, 0.3),
0 15px 35px rgba(124, 77, 255, 0.2),
inset 0 2px 15px rgba(255, 255, 255, 0.8);
border: 6px solid;
border-image: linear-gradient(45deg, #ff4081, #7C4DFF, #40C4FF) 1;
position: relative;
overflow: hidden;
opacity: 0;
animation: fadeIn 0.8s ease-out 0.3s forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.header {
text-align: center;
margin-bottom: 45px;
}
.title {
font-family: 'Times New Roman', Times, serif;
font-size: 4.5em;
background: linear-gradient(45deg, #ff4081, #7C4DFF, #40C4FF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 25px 0;
text-shadow: 3px 3px 15px rgba(255, 64, 129, 0.2);
font-weight: bold;
}
.hearts {
display: flex;
justify-content: center;
gap: 35px;
margin: 25px 0;
}
.heart {
font-size: 3.5em;
filter: drop-shadow(0 5px 12px rgba(255, 64, 129, 0.4));
animation: heartMoveSync 2s infinite ease-in-out;
}
@keyframes heartMoveSync {
0%, 100% { transform: translateY(0) scale(1); }
25% { transform: translateY(-15px) scale(1.2); }
50% { transform: translateY(0) scale(1); }
75% { transform: translateY(-15px) scale(1.2); }
}
.main-content {
display: flex;
flex-wrap: wrap;
gap: 55px;
align-items: center;
justify-content: center;
margin: 45px 0;
}
.cake-section {
flex: 1;
min-width: 320px;
text-align: center;
}
.cake {
width: 270px;
height: 240px;
margin: 0 auto 35px;
position: relative;
cursor: pointer;
transition: transform 0.2s ease;
}
.cake:hover {
transform: scale(1.03);
}
.cake-layer {
position: absolute;
border-radius: 18px;
box-shadow:
0 10px 25px rgba(0,0,0,0.2),
inset 0 -6px 12px rgba(0,0,0,0.15);
}
.cake-layer.bottom {
width: 240px;
height: 90px;
background: linear-gradient(45deg, #ff6b9d, #ff4081);
bottom: 0;
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
.cake-layer.middle {
width: 200px;
height: 80px;
background: linear-gradient(45deg, #ff9ebb, #ff6b9d);
bottom: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 2;
}
.cake-layer.top {
width: 160px;
height: 70px;
background: linear-gradient(45deg, #ffccd5, #ff9ebb);
bottom: 150px;
left: 50%;
transform: translateX(-50%);
border-radius: 50% 50% 25% 25%;
z-index: 3;
}
.candle {
position: absolute;
bottom: 150px;
left: 50%;
transform: translateX(-50%);
z-index: 4;
}
.candle-stick {
width: 18px;
height: 45px;
background: linear-gradient(to bottom, #FFD700, #FF9800);
border-radius: 9px 9px 0 0;
margin: 0 auto;
box-shadow: 0 6px 20px rgba(255, 152, 0, 0.4);
position: relative;
top: -12px;
}
.flame {
width: 30px;
height: 40px;
background: radial-gradient(circle, #FFEB3B 0%, #FF9800 70%, transparent 90%);
border-radius: 50% 50% 25% 25%;
margin: 0 auto;
opacity: 0;
filter: blur(2px);
position: absolute;
top: -40px; /* ИСПРАВЛЕНО: поднял огонь выше */
left: 50%;
transform: translateX(-50%);
z-index: 5;
}
.flame.lit {
opacity: 1;
animation: flameFlicker 0.6s infinite alternate;
}
@keyframes flameFlicker {
0% { transform: translateX(-50%) scale(1) rotate(-8deg); }
100% { transform: translateX(-50%) scale(1.4) rotate(8deg); }
}
.slider-controls {
margin-top: 10px;
}
.slider-group {
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 25px;
border: 3px solid #40C4FF;
margin-bottom: 15px;
}
.slider-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.slider-group label {
font-weight: bold;
color: #7C4DFF;
font-size: 1.3em;
flex-grow: 1;
margin-left: 12px;
}
.slider-value {
font-weight: bold;
color: #FF4081;
font-size: 1.3em;
min-width: 70px;
text-align: right;
}
.range-slider {
width: 100%;
height: 30px;
-webkit-appearance: none;
appearance: none;
background: linear-gradient(to right, #40C4FF, #7C4DFF);
border-radius: 20px;
outline: none;
cursor: pointer;
}
.range-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 40px;
height: 40px;
background: white;
border-radius: 50%;
cursor: pointer;
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.4),
0 0 0 4px #FFD700;
border: 3px solid white;
}
.cake-hint {
background: rgba(255, 255, 255, 0.9);
padding: 12px 20px;
border-radius: 15px;
border: 2px solid #FFD700;
text-align: center;
font-size: 0.9em;
color: #7C4DFF;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
animation: hintPulse 2s infinite;
}
@keyframes hintPulse {
0%, 100% { opacity: 0.9; }
50% { opacity: 1; }
}
.hint-icon {
font-size: 1.2em;
animation: hintBounce 1s infinite;
}
@keyframes hintBounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
.hint-text {
font-weight: 500;
}
.message-section {
flex: 1;
min-width: 320px;
}
.greeting-message {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 240, 245, 0.95));
padding: 35px;
border-radius: 30px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
border: 3px dashed #ff4081;
}
.greeting-message h2 {
color: #7C4DFF;
margin-bottom: 25px;
font-size: 2.5em;
font-weight: bold;
}
.highlight {
color: #FF4081;
font-weight: bold;
text-shadow: 2px 2px 6px rgba(255, 64, 129, 0.3);
}
.wish {
font-size: 1.5em;
line-height: 1.9;
color: #333;
text-align: center;
font-weight: 500;
}
.particle {
position: fixed;
pointer-events: none;
z-index: 10000;
}
.confetti-piece {
width: 15px;
height: 15px;
border-radius: 4px;
}
.heart-particle {
font-size: 2em;
filter: drop-shadow(0 0 15px rgba(255, 0, 100, 0.9));
}
.sparkle-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.firework-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.spiral-particle {
width: 9px;
height: 9px;
border-radius: 50%;
}
.rainbow-particle {
width: 14px;
height: 14px;
border-radius: 50%;
}
.lightning-particle {
width: 8px;
height: 35px;
border-radius: 4px;
}
.message-popup {
position: fixed;
top: -100px;
left: 50% !important;
transform: translateX(-50%) !important;
background: rgba(255, 255, 255, 0.97);
padding: 20px 40px;
border-radius: 60px;
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
z-index: 10001;
font-weight: bold;
color: #7C4DFF;
border: 4px solid #FF4081;
font-family: 'Times New Roman', Times, serif;
text-align: center;
max-width: 90%;
font-size: 1.3em;
white-space: nowrap;
}
@media (max-width: 768px) {
.message-popup {
white-space: normal;
max-width: 85%;
padding: 15px 25px;
font-size: 1.1em;
}
}
.flash-effect {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 9999;
pointer-events: none;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.card {
padding: 25px 20px;
margin: 10px;
}
.title {
font-size: 2.8em;
}
.main-content {
flex-direction: column;
gap: 30px;
}
.envelope {
width: 280px;
height: 190px;
}
.name-highlight {
font-size: 28px;
}
.heart {
font-size: 2.5em;
}
.cake-hint {
padding: 10px 15px;
font-size: 0.85em;
}
.greeting-message h2 {
font-size: 2em;
}
.wish {
font-size: 1.3em;
}
.cake {
width: 240px;
height: 210px;
}
.cake-layer.bottom {
width: 210px;
height: 80px;
}
.cake-layer.middle {
width: 170px;
height: 70px;
bottom: 70px;
}
.cake-layer.top {
width: 130px;
height: 60px;
bottom: 130px;
}
.candle {
bottom: 130px;
}
.candle-stick {
height: 40px;
}
}HTML
<!DOCTYPE html>
<html lang="ru">
<head>
<title>🎂 С Днём Рождения, Роман!</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="envelopeScreen" class="envelope-screen">
<div class="envelope-container">
<div class="envelope" id="envelope">
<div class="envelope-front">
<div class="envelope-flap"></div>
<div class="envelope-body">
<div class="envelope-design"></div>
<div class="stamp">
<span class="stamp-text">🎂</span>
</div>
<div class="to-label">
<div class="label-text">Для:</div>
<div class="name-highlight">Романа</div>
</div>
</div>
</div>
<div class="envelope-back">
<div class="envelope-message">
<div class="message-icon">✨</div>
<div class="message-text">С ДР!</div>
<div class="message-hint">✨</div>
</div>
</div>
</div>
<div class="open-instruction">
<div class="instruction-text">Нажми на конверт, чтобы получить счастье</div>
</div>
</div>
</div>
<div id="cardContent" class="card-content-wrapper">
<div class="container">
<div class="card">
<div class="card-content">
<div class="header">
<h1 class="title">🎉 С Днём Рождения! 🎉</h1>
<div class="hearts">
<div class="heart">❤️</div>
<div class="heart">💖</div>
<div class="heart">💕</div>
</div>
</div>
<div class="main-content">
<div class="cake-section">
<div class="cake" id="interactiveCake">
<div class="cake-layer bottom"></div>
<div class="cake-layer middle"></div>
<div class="cake-layer top"></div>
<div class="candle">
<div class="candle-stick"></div>
<div class="flame" id="flame"></div>
</div>
</div>
<div class="slider-controls">
<div class="slider-group">
<div class="slider-header">
<span class="slider-icon">✨</span>
<label>Зажечь свечу:</label>
<span class="slider-value" id="candleValue">Выкл</span>
</div>
<input type="range" class="range-slider" id="candleSlider" min="0" max="1" step="1" value="0">
</div>
<div class="cake-hint">
<span class="hint-icon">👇</span>
<span class="hint-text">Нажимай на торт для пожеланий и эффектов</span>
</div>
</div>
</div>
<div class="message-section">
<div class="greeting-message">
<h2>Дорогой <span id="displayName" class="highlight">Роман</span>!</h2>
<p class="wish">Пусть сегодня сбудутся все мечты,<br>А каждый миг приносит лишь добро!<br>Удачи, денег, радости, любви,<br>И чтобы жизнь была как волшебство!</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>3. Итоги
3.1 Что получилось в итоге
В результате разработки была создана поздравительная открытка: представляет собой готовый проект, который можно отправлять, но также можно и добавить что-то своё. Проект показывает, как даже с помощью не самого сложного кода можно создать памятное воспоминание.
3.2 Потенциал для развития.
Этот проект лишь начало (мне он нравится, и он готов, но можно улучшить). Но идея самого проекта позволяет реализовать множество интересных улучшений и создавать совершенно уникальные версии открыток. Вот несколько направлений для вдохновения:
Собственные темы и стили, например, новогодняя версия с падающим снегом, рождественская с тёплым камином или весенняя с цветущей сакурой.
Мультимедийные возможности: голосовые или видео поздравления.
Интерактивные истории: квест-открытки с последовательностью заданий и сюрпризов в конце, вот этот вариант мне нравится больше всего и, скорее всего, его ещё отельную версию напишу.
3.3 Заключение
Этот проект — напоминание о том, что порой не нужно ничего искать или покупать: приятное ощущение в нашем мире можно создать с помощью технологий, которые могут быть не просто функциональными, но и душевными, бережными, способными передать главное — то, что ты хотел поздравить любимого и дорогого человека.
Мы живём в эпоху, когда сообщение легко заменяет разговор за чашкой чая, но такие проекты напоминают, что технологии — вовсе не помеха для чувств, а, наоборот, прекрасный инструмент, чтобы эти чувства усилить. Они позволяют нам создавать моменты, которые запоминаются, т.к. то, что рождено в интернете, не вечно, но всё-таки на очень долгое время.
Этот проект — приглашение к творчеству, потому что в него можно вложить всё, что подсказывает сердце: свои слова, свои образы.
Открытка доступна онлайн — вы можете испытать её сами.
Полный исходный код опубликован на GitHub — используйте его как основу для своих творческих экспериментов.
Если у вас есть идеи по улучшению проекта или вы заметили какие-то недочёты — буду искренне рад вашей обратной связи. Лучшие предложения могут быть реализованы в следующих версиях или стать отправной точкой для совершенно новых проектов.
© 2026 ООО «МТ ФИНАНС»