Enhance birthday feature: add garland decoration, improve balloon pop confetti effect, and refine animation dynamics
This commit is contained in:
@@ -10,6 +10,23 @@
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.birthday-garland {
|
||||
position: absolute;
|
||||
top: -1vh;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 55;
|
||||
pointer-events: none;
|
||||
/* optional: a little drop shadow for depth */
|
||||
filter: drop-shadow(0 5px 8px rgba(0,0,0,0.5));
|
||||
}
|
||||
|
||||
.birthday-garland img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.birthday-cake {
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
@@ -18,6 +35,7 @@
|
||||
font-size: 8rem;
|
||||
z-index: 50;
|
||||
filter: drop-shadow(0 0 10px rgba(255,255,255,0.4));
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.birthday-cake img {
|
||||
@@ -34,6 +52,13 @@
|
||||
font-size: 3rem;
|
||||
opacity: 0.95;
|
||||
z-index: 40;
|
||||
pointer-events: none; /* Container itself should not block clicks */
|
||||
}
|
||||
|
||||
.birthday-inner {
|
||||
pointer-events: auto; /* Allow hover over the actual item */
|
||||
cursor: crosshair;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.birthday-symbol img {
|
||||
@@ -88,3 +113,50 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes birthday-sway {
|
||||
0% { transform: rotate(-8deg) translateX(-5%); }
|
||||
100% { transform: rotate(8deg) translateX(5%); }
|
||||
}
|
||||
|
||||
@keyframes birthday-pop {
|
||||
0% { transform: scale(1); opacity: 1; filter: brightness(1); }
|
||||
30% { transform: scale(1.3); opacity: 1; filter: brightness(1.5); }
|
||||
100% { transform: scale(0); opacity: 0; filter: brightness(2); }
|
||||
}
|
||||
|
||||
.birthday-burst-confetti {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
will-change: transform, opacity;
|
||||
animation: birthday-burst-fall 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
}
|
||||
|
||||
.birthday-burst-confetti.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.birthday-burst-confetti.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent !important;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 10px solid var(--shape-color, #ff0000);
|
||||
}
|
||||
|
||||
@keyframes birthday-burst-fall {
|
||||
0% {
|
||||
transform: translate(0, 0) rotate3d(var(--rx), var(--ry), var(--rz), 0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
30% {
|
||||
transform: translate(var(--burst-x), var(--burst-y)) rotate3d(var(--rx), var(--ry), var(--rz), calc(var(--rot-dir) * 0.3));
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(var(--burst-x), calc(var(--burst-y) + 150px)) rotate3d(var(--rx), var(--ry), var(--rz), var(--rot-dir));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@ const symbolCount = config.SymbolCount || 25;
|
||||
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
|
||||
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
const enableGarland = config.EnableGarland !== undefined ? config.EnableGarland : true;
|
||||
|
||||
const birthdayImages = [
|
||||
'../Seasonals/Resources/birthday_images/balloon_1.gif',
|
||||
'../Seasonals/Resources/birthday_images/balloon_2.gif',
|
||||
'../Seasonals/Resources/birthday_images/balloon_3.gif'
|
||||
];
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
@@ -39,6 +46,69 @@ observer.observe(document.body, {
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createBalloonPopConfetti(container, x, y) {
|
||||
const popConfettiColors = [
|
||||
'#fce18a', '#ff726d', '#b48def', '#f4306d',
|
||||
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
|
||||
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
|
||||
];
|
||||
|
||||
// Spawn 15-20 particles
|
||||
const particleCount = Math.floor(Math.random() * 5) + 15;
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.classList.add('birthday-burst-confetti');
|
||||
|
||||
// Random color
|
||||
const color = popConfettiColors[Math.floor(Math.random() * popConfettiColors.length)];
|
||||
particle.style.backgroundColor = color;
|
||||
|
||||
// Random shape
|
||||
const shape = Math.random();
|
||||
if (shape > 0.66) {
|
||||
particle.classList.add('circle');
|
||||
const size = Math.random() * 4 + 4; // 4-8px
|
||||
particle.style.width = `${size}px`;
|
||||
particle.style.height = `${size}px`;
|
||||
} else if (shape > 0.33) {
|
||||
particle.classList.add('rect');
|
||||
const width = Math.random() * 3 + 3; // 3-6px
|
||||
const height = Math.random() * 4 + 6; // 6-10px
|
||||
particle.style.width = `${width}px`;
|
||||
particle.style.height = `${height}px`;
|
||||
} else {
|
||||
particle.classList.add('triangle');
|
||||
}
|
||||
|
||||
particle.style.position = 'absolute';
|
||||
particle.style.left = `${x}px`;
|
||||
particle.style.top = `${y}px`;
|
||||
particle.style.zIndex = '1000';
|
||||
|
||||
// Random direction for explosion (circular)
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
const distance = Math.random() * 60 + 20; // 20-80px burst radius
|
||||
|
||||
const xOffset = Math.cos(angle) * distance;
|
||||
const yOffset = Math.sin(angle) * distance;
|
||||
|
||||
particle.style.setProperty('--burst-x', `${xOffset}px`);
|
||||
particle.style.setProperty('--burst-y', `${yOffset}px`);
|
||||
|
||||
// Random rotation during fall
|
||||
particle.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
|
||||
particle.style.setProperty('--rx', Math.random().toFixed(2));
|
||||
particle.style.setProperty('--ry', Math.random().toFixed(2));
|
||||
particle.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||
|
||||
container.appendChild(particle);
|
||||
|
||||
// Remove particle after animation
|
||||
setTimeout(() => particle.remove(), 1500);
|
||||
}
|
||||
}
|
||||
|
||||
function createBirthday() {
|
||||
const container = document.querySelector('.birthday-container') || document.createElement('div');
|
||||
|
||||
@@ -48,11 +118,22 @@ function createBirthday() {
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Party Garland
|
||||
if (enableGarland) {
|
||||
const garland = document.createElement('div');
|
||||
garland.className = 'birthday-garland';
|
||||
const garlandImg = document.createElement('img');
|
||||
garlandImg.src = '../Seasonals/Resources/birthday_images/garland.png';
|
||||
garlandImg.onerror = function() { this.style.display = 'none'; };
|
||||
garland.appendChild(garlandImg);
|
||||
container.appendChild(garland);
|
||||
}
|
||||
|
||||
// Spawn Birthday Cake at the bottom
|
||||
const cake = document.createElement('div');
|
||||
cake.className = 'birthday-cake';
|
||||
let cakeImg = document.createElement('img');
|
||||
cakeImg.src = `../Seasonals/Resources/birthday_images/cake.png`;
|
||||
cakeImg.src = `../Seasonals/Resources/birthday_images/cake.gif`;
|
||||
cakeImg.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
this.parentElement.innerHTML = '🎂';
|
||||
@@ -72,8 +153,8 @@ function createBirthday() {
|
||||
|
||||
const useRandomDuration = enableDifferentDuration !== false;
|
||||
|
||||
// We'll treat balloons and gifts as rising symbols
|
||||
const activeItems = ['balloon_red', 'balloon_blue', 'balloon_yellow', 'gift'];
|
||||
// We'll treat all balloons as rising symbols
|
||||
const activeItems = ['balloon_1', 'balloon_2', 'balloon_3'];
|
||||
|
||||
for (let i = 0; i < finalCount; i++) {
|
||||
let symbol = document.createElement('div');
|
||||
@@ -81,20 +162,59 @@ function createBirthday() {
|
||||
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
|
||||
symbol.className = `birthday-symbol birthday-${randomItem}`;
|
||||
|
||||
// Create inner div for sway
|
||||
let innerDiv = document.createElement('div');
|
||||
innerDiv.className = 'birthday-inner';
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.src = `../Seasonals/Resources/birthday_images/${randomItem}.png`;
|
||||
img.src = `../Seasonals/Resources/birthday_images/${randomItem}.gif`; // Use standard pop GIFs
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
this.parentElement.innerHTML = getBirthdayEmojiFallback(randomItem);
|
||||
};
|
||||
symbol.appendChild(img);
|
||||
innerDiv.appendChild(img);
|
||||
symbol.appendChild(innerDiv);
|
||||
|
||||
const leftPos = Math.random() * 95;
|
||||
const delaySeconds = Math.random() * 10;
|
||||
|
||||
// Far away effect
|
||||
const depth = Math.random();
|
||||
const scale = 0.5 + depth * 0.7; // 0.5 to 1.2
|
||||
const zIndex = Math.floor(depth * 30) + 10;
|
||||
|
||||
img.style.transform = `scale(${scale})`;
|
||||
symbol.style.zIndex = zIndex;
|
||||
|
||||
let durationSeconds = 9;
|
||||
if (useRandomDuration) {
|
||||
durationSeconds = Math.random() * 5 + 7; // 7 to 12 seconds
|
||||
// Far strings climb slower
|
||||
durationSeconds = (1 - depth) * 6 + 7 + Math.random() * 4;
|
||||
}
|
||||
|
||||
const isBalloon = randomItem.startsWith('balloon');
|
||||
|
||||
if (isBalloon) {
|
||||
// Sway animation
|
||||
const swayDur = Math.random() * 2 + 3; // 3 to 5s
|
||||
const swayDir = Math.random() > 0.5 ? 'normal' : 'reverse';
|
||||
innerDiv.style.animation = `birthday-sway ${swayDur}s ease-in-out infinite alternate ${swayDir}`;
|
||||
|
||||
// Interaction to pop is handled visually by the GIF, but we can still remove it on hover
|
||||
innerDiv.addEventListener('mouseenter', function(e) {
|
||||
if (!this.classList.contains('popped')) {
|
||||
this.classList.add('popped');
|
||||
this.style.animation = 'birthday-pop 0.2s ease-out forwards';
|
||||
this.style.pointerEvents = 'none'; // avoid re-triggering
|
||||
|
||||
// Create confetti burst at balloon's screen position
|
||||
const rect = this.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
// Ensure the burst container is appended to the main document body or the birthday container
|
||||
createBalloonPopConfetti(document.body, cx, cy);
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
const startRot = (Math.random() * 20) - 10; // -10 to +10 spread
|
||||
|
||||
Reference in New Issue
Block a user