javascript

Интерактивная поздравительная открытка на JavaScript

  • среда, 11 февраля 2026 г. в 00:00:06
https://habr.com/ru/companies/ruvds/articles/993246/

1. Введение:

1.1 Идея проекта — эмоции в цифровом формате.

Типичная картинка с котом и надписью «Happy Birthday» — именно такие открытки чаще всего используют. Вариантов, конечно, тысячи, но суть одна.
Типичная картинка с котом и надписью «Happy Birthday» — именно такие открытки чаще всего используют. Вариантов, конечно, тысячи, но суть одна.

Наверное, каждый из нас ловил себя на мысли: что отправить на день рождения в этот раз? Просто текст, тёплую фотографию или голосовое сообщение? И сразу вспоминается это чувство, когда ищешь или обдумываешь креативный текст, а потом вспоминаешь о милой картинке с котиком, гифке с шампанским или стандартном «С ДР!» — и отправляешь, чисто для галочки.

Со временем я заметил: когда получаешь такое поздравление в Telegram, становится немного грустно, что человек хоть и поздравил, но не потратил время на то, чтобы обдумать и искренне пожелать чего-то хорошего и уникального только про тебя. Хотя понимаешь — это нормально. Поколение постарше привыкло обмениваться этими милыми, но безликими посланиями. Хотя не все делают это из-за нежелания — некоторым просто понравилась картинка, вот и скинули.

А недавно за чаем друг рассказал историю о том, как в 10-м классе поздравил одноклассницу с днём рождения. Он решил, что написать в сообщение или купить бумажную открытку — слишком просто. Попытался сделать что-то своими руками, но не получилось (если говорить кратко). Тогда он создал простую электронную открытку, где при тряске экрана падало конфетти. «Ты открываешь на телефоне — трясёшь, и бах!» — смеялся он, вспоминая. Девочке открытка понравилась именно потому, что была сделана специально для неё, своими руками. Если подумать — она не выглядела профессионально или красиво, но эта ручная работа, сделанная с душой, заставила её восхититься.

После этой беседы меня осенило: почему бы не сделать нечто подобное сейчас? Ведь такая открытка до сих пор остаётся необычной и уникальной — она не стала обыденной вещью. Не искать в Яндексе очередную картинку, а создать интерактивную историю, с которой можно взаимодействовать. Чтобы человек не просто пролистал фотографию, а почувствовал: это сделано именно для него.

Вот как выглядит сам проект: интерактивная открытка, в которой каждый элемент реагирует на действия пользователя. Сначала идёт конверт — не просто картинка, а элемент, который можно «взять» и открыть. Затем появляется торт со свечой. Свечу можно зажечь слайдером — переключаешь, и она загорается. А если нажать на сам торт — выскакивает случайное пожелание и запускается один из семи эффектов: летящие сердца, фейерверки и др. Конечно, в будущем можно придумать что-то более уникальное, но для первой версии подойдёт — главное, передать идею. Может, кто-то из вас, читающих эту статью, подскажет, что лучше добавить вместо этих эффектов.

Всё строится на чистом HTML, CSS и JavaScript — без всяких сложностей. Я считаю, чем проще, тем больше людей смогут его понять и посмотреть. В проекте важно сохранить ауру Handmade, чтобы поздравление воспринималось как эксклюзив.

Это пока только прототип — основа, которую можно персонализировать, но уже сейчас я вижу, что в мире, где все обмениваются однотипными картинками, можно подарить уникальный, сделанный вручную (хоть и в коде) открытку.

В итоге мы же поздравляем не для вида. Мы хотим сказать: «Ты занимаешь важное место в моих мыслях, и это время, потраченное на поиск нужных слов, — лишь малая часть той теплоты, которую я к тебе чувствую». И иногда для этого действительно достаточно просто отправить котика, но иногда даже созданного маленького мира не хватает, чтобы выразить всё, что хочешь сказать человеку.

1.2 Подготовка к разработке

Для создания проекта не требовалось сложных инструментов. Весь код заключён в трёх основных технологиях, которые прекрасно работают в любом современном браузере:

Три основных типа файлов
Три основных типа файлов

Технологический стек:

HTML — семантическая разметка и структура

CSS — визуальное оформление, анимации, адаптивный дизайн

JavaScript  — интерактивность и логика работы

Структура проекта:

birthday-card/
├── index.html              # Основная структура открытки
├── styles.css              # Все стили и анимации
└── script.js               # Вся интерактивная логика

Прелесть этого проекта в простоте — его можно легко назвать чистым кодом. Открытка работает в любом браузере: от настольного компьютера до мобильного телефона.


2. Основная часть

2.1 Архитектура взаимодействия.

Цель проекта — создать цифровой аналог реальных действий, которые ассоциируются с праздником. Дать почувствовать, будто пользователь держит в руках не пиксели, а настоящую открытку, бережно сделанную вручную. Каждый элемент открытки был сделан так, чтобы были приятные ощущения.

Основные части проекта:

  1. Конверт — традиционный элемент любого поздравления, который нужно «открыть».

  2. Свеча на торте — символ праздника, который обычно задувают, но прежде нужно зажечь.

  3. Сам торт — главный элемент, реагирующий на прикосновения.

Каждая часть была реализована с учётом реального опыта поздравления — того, как мы обычно поздравляем вживую. Например, анимация конверта использует принцип предвкушения: сначала пользователь видит красивый конверт и только когда его открывает, видит содержимое.

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) озаряет всё вокруг, создавая ощущение чуда. И в этот самый момент из центра конверта вырывается пучок праздничных конфетти — сорок разноцветных частиц, летящих во все стороны с естественной, «бумажной» траекторией (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 ООО «МТ ФИНАНС»