' +
+ // Delay success message slightly to let celebration start first
+ setTimeout(function() {
+ // Create success HTML with Tailwind classes
+ var successHtml = '
' +
'
' +
'
' + queriesDone + '
' +
@@ -204,15 +211,16 @@ function displaySuccessInPage(stats) {
'
' +
'
';
- // Find main content area and replace content
- var mainContent = document.querySelector('main');
- if (mainContent) {
- // Clear all content and show success message
- mainContent.innerHTML = successHtml;
- }
+ // Find main content area and replace content
+ var mainContent = document.querySelector('main');
+ if (mainContent) {
+ // Clear all content and show success message
+ mainContent.innerHTML = successHtml;
+ }
+ }, 500); // 500ms delay for celebration to start
}
/**
@@ -380,6 +388,35 @@ function updateProgress(data) {
var stats = data.stats;
var session = data.session;
+ // Fix 99.5% bug: Force 100% display when import is finished
+ // The offset/fileSize calculation can plateau at 99.5% due to rounding
+ if (data.finished) {
+ var progressBar = document.querySelector('.progress-bar');
+ if (progressBar) {
+ progressBar.style.width = '100%';
+ progressBar.classList.add('progress-bar-complete');
+ }
+ // Force all percentage displays to show 100%
+ var statBoxes = document.querySelectorAll('.stat-box .stat-value');
+ if (statBoxes[3]) {
+ statBoxes[3].textContent = '100.0%';
+ }
+ var pctDisplay = document.querySelector('#elapsedTimer + div');
+ if (pctDisplay) {
+ pctDisplay.textContent = '100.00% Complete';
+ }
+ // Update table percentage row to show 100%
+ var table = document.querySelector('table tbody');
+ if (table) {
+ var rows = table.getElementsByTagName('tr');
+ if (rows[5]) { // Row 6 = Percentage
+ var cells = rows[5].getElementsByTagName('td');
+ if (cells[2]) cells[2].textContent = '100.00'; // pct_done column
+ }
+ }
+ return; // Don't process further updates when finished
+ }
+
// Update line number display (using textContent for XSS safety)
if (session && session.start !== undefined) {
var paragraphs = document.getElementsByTagName('p');
diff --git a/templates/import.php b/templates/import.php
index c47c2ab..7748198 100644
--- a/templates/import.php
+++ b/templates/import.php
@@ -371,6 +371,7 @@
+
= $ajaxScript ?>
= $redirectScript ?>
From 23d8f5782079d8003dacef4bfa8ea6c93c8c067e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?=
<8407711+w3spi5@users.noreply.github.com>
Date: Tue, 13 Jan 2026 19:13:17 +0100
Subject: [PATCH 05/10] enhance completion screen dark theme + realistic
fireworks/confetti effects
---
assets/css/bigdump.css | 28 ++++
assets/js/celebration.js | 310 ++++++++++++++++++++++++++++-------
assets/src/css/tailwind.css | 28 ++++
assets/src/js/celebration.js | 310 ++++++++++++++++++++++++++++-------
src/Services/AjaxService.php | 24 +--
5 files changed, 564 insertions(+), 136 deletions(-)
diff --git a/assets/css/bigdump.css b/assets/css/bigdump.css
index 8424456..1dfea87 100644
--- a/assets/css/bigdump.css
+++ b/assets/css/bigdump.css
@@ -859,6 +859,34 @@
}
}
+/* Success stat cards with enhanced dark mode */
+.stat-card-success {
+ background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
+ border-radius: 0.75rem;
+ padding: 1rem;
+ border: 1px solid rgba(34, 197, 94, 0.2);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.stat-card-success:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(34, 197, 94, 0.15);
+}
+
+.dark .stat-card-success {
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.25) 100%);
+ border: 1px solid rgba(74, 222, 128, 0.3);
+ box-shadow: 0 0 20px rgba(34, 197, 94, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+.dark .stat-card-success:hover {
+ box-shadow: 0 0 25px rgba(34, 197, 94, 0.2),
+ 0 4px 15px rgba(0, 0, 0, 0.3),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
+ border-color: rgba(74, 222, 128, 0.5);
+}
+
/* ========================================
FILE UPLOAD ENHANCEMENTS
======================================== */
diff --git a/assets/js/celebration.js b/assets/js/celebration.js
index 7343c46..5d80a74 100644
--- a/assets/js/celebration.js
+++ b/assets/js/celebration.js
@@ -1,7 +1,7 @@
/**
- * BigDump - Celebration Effects Module
- * Creates fireworks and confetti animations on import completion.
- * Uses Canvas API for particle-based visual effects.
+ * BigDump - Celebration Effects Module (Enhanced)
+ * Creates realistic fireworks and confetti animations on import completion.
+ * Features: particle trails, glow effects, 3D confetti rotation, air resistance.
*/
(function() {
'use strict';
@@ -11,38 +11,63 @@
var animationId = null;
var particles = [];
var confetti = [];
+ var trails = [];
var isRunning = false;
var startTime = 0;
- var duration = 4000; // 4 seconds of celebration
-
- // Colors for fireworks and confetti
- var fireworkColors = [
- '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
- '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
+ var fireworksDuration = 20000; // 20 seconds of fireworks
+ var confettiForever = true; // Confetti falls indefinitely
+
+ // Vibrant colors for fireworks
+ var fireworkPalettes = [
+ ['#FF6B6B', '#FF8E8E', '#FFB4B4'], // Red
+ ['#4ECDC4', '#7EDCD6', '#A8EBE7'], // Teal
+ ['#45B7D1', '#6FC9DE', '#99DBEB'], // Blue
+ ['#FFD93D', '#FFE469', '#FFEF9A'], // Yellow
+ ['#6BCB77', '#8DD896', '#AFE5B5'], // Green
+ ['#FF8C00', '#FFA533', '#FFBE66'], // Orange
+ ['#DA70D6', '#E494E0', '#EEB8EA'], // Orchid
+ ['#9B59B6', '#B07CC6', '#C59FD6'], // Purple
+ ['#E74C3C', '#ED7669', '#F3A096'], // Coral
+ ['#3498DB', '#5DADE2', '#85C1E9'] // Sky blue
];
var confettiColors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77',
- '#FF8C00', '#DA70D6', '#20B2AA', '#FF69B4', '#87CEEB'
+ '#FF8C00', '#DA70D6', '#9B59B6', '#E74C3C', '#3498DB',
+ '#F39C12', '#1ABC9C', '#E91E63', '#00BCD4', '#8BC34A'
];
/**
- * Particle class for fireworks
+ * Enhanced Particle class with trails and glow
*/
- function Particle(x, y, color, velocity, gravity, decay, size) {
+ function Particle(x, y, color, velocity, options) {
+ options = options || {};
this.x = x;
this.y = y;
this.color = color;
this.velocity = velocity;
- this.gravity = gravity || 0.05;
- this.decay = decay || 0.015;
+ this.gravity = options.gravity || 0.06;
+ this.decay = options.decay || 0.015;
this.alpha = 1;
- this.size = size || 3;
+ this.size = options.size || 3;
+ this.hasTrail = options.hasTrail !== false;
+ this.glowSize = options.glowSize || this.size * 2;
+ this.friction = options.friction || 0.98;
+ this.trail = [];
+ this.maxTrailLength = options.trailLength || 5;
}
Particle.prototype.update = function() {
- this.velocity.x *= 0.99;
- this.velocity.y *= 0.99;
+ // Store trail position
+ if (this.hasTrail && this.alpha > 0.3) {
+ this.trail.push({ x: this.x, y: this.y, alpha: this.alpha });
+ if (this.trail.length > this.maxTrailLength) {
+ this.trail.shift();
+ }
+ }
+
+ this.velocity.x *= this.friction;
+ this.velocity.y *= this.friction;
this.velocity.y += this.gravity;
this.x += this.velocity.x;
this.y += this.velocity.y;
@@ -51,85 +76,202 @@
};
Particle.prototype.draw = function(ctx) {
+ // Draw trail
+ if (this.hasTrail && this.trail.length > 0) {
+ for (var i = 0; i < this.trail.length; i++) {
+ var t = this.trail[i];
+ var trailAlpha = (i / this.trail.length) * this.alpha * 0.5;
+ var trailSize = this.size * (0.3 + (i / this.trail.length) * 0.5);
+ ctx.save();
+ ctx.globalAlpha = trailAlpha;
+ ctx.beginPath();
+ ctx.arc(t.x, t.y, trailSize, 0, Math.PI * 2);
+ ctx.fillStyle = this.color;
+ ctx.fill();
+ ctx.restore();
+ }
+ }
+
+ // Draw glow
+ ctx.save();
+ ctx.globalAlpha = this.alpha * 0.4;
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2);
+ var gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.glowSize);
+ gradient.addColorStop(0, this.color);
+ gradient.addColorStop(1, 'transparent');
+ ctx.fillStyle = gradient;
+ ctx.fill();
+ ctx.restore();
+
+ // Draw core
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
+
+ // Bright center
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.size * 0.5, 0, Math.PI * 2);
+ ctx.fillStyle = '#FFFFFF';
+ ctx.globalAlpha = this.alpha * 0.8;
+ ctx.fill();
ctx.restore();
};
/**
- * Confetti piece class
+ * Enhanced Confetti with 3D rotation and air resistance
*/
function ConfettiPiece(x, y, color) {
this.x = x;
this.y = y;
this.color = color;
- this.width = Math.random() * 10 + 5;
- this.height = Math.random() * 6 + 3;
+ this.width = Math.random() * 12 + 6;
+ this.height = Math.random() * 8 + 4;
this.velocity = {
- x: (Math.random() - 0.5) * 6,
- y: Math.random() * 3 + 2
+ x: (Math.random() - 0.5) * 8,
+ y: Math.random() * 2 + 1
};
- this.rotation = Math.random() * 360;
- this.rotationSpeed = (Math.random() - 0.5) * 10;
- this.oscillationSpeed = Math.random() * 0.1 + 0.05;
- this.oscillationDistance = Math.random() * 40 + 20;
+ // 3D rotation angles
+ this.rotationX = Math.random() * 360;
+ this.rotationY = Math.random() * 360;
+ this.rotationZ = Math.random() * 360;
+ this.rotationSpeedX = (Math.random() - 0.5) * 8;
+ this.rotationSpeedY = (Math.random() - 0.5) * 12;
+ this.rotationSpeedZ = (Math.random() - 0.5) * 6;
+ // Oscillation (swaying)
+ this.oscillationSpeed = Math.random() * 0.08 + 0.03;
+ this.oscillationDistance = Math.random() * 60 + 30;
this.initialX = x;
this.time = Math.random() * 100;
+ // Air resistance
+ this.drag = 0.98 + Math.random() * 0.015;
+ this.terminalVelocity = 4 + Math.random() * 2;
}
ConfettiPiece.prototype.update = function(canvasHeight) {
this.time += this.oscillationSpeed;
+
+ // Horizontal oscillation (swaying in wind)
this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance;
+ this.initialX += this.velocity.x * 0.1; // Drift
+
+ // Apply drag
+ this.velocity.x *= this.drag;
+ this.velocity.y += 0.08; // Gravity
+
+ // Terminal velocity
+ if (this.velocity.y > this.terminalVelocity) {
+ this.velocity.y = this.terminalVelocity;
+ }
+
this.y += this.velocity.y;
- this.rotation += this.rotationSpeed;
- this.velocity.y += 0.02; // Light gravity
+
+ // Tumbling rotation
+ this.rotationX += this.rotationSpeedX;
+ this.rotationY += this.rotationSpeedY;
+ this.rotationZ += this.rotationSpeedZ;
+
+ // Slow down rotation over time
+ this.rotationSpeedX *= 0.999;
+ this.rotationSpeedY *= 0.999;
+
return this.y < canvasHeight + 50;
};
ConfettiPiece.prototype.draw = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
- ctx.rotate(this.rotation * Math.PI / 180);
+
+ // Apply 3D-like rotation (fake perspective)
+ var scaleX = Math.cos(this.rotationY * Math.PI / 180);
+ var scaleY = Math.cos(this.rotationX * Math.PI / 180);
+ ctx.rotate(this.rotationZ * Math.PI / 180);
+ ctx.scale(scaleX, scaleY);
+
+ // Draw confetti piece with slight shadow
+ ctx.shadowColor = 'rgba(0,0,0,0.2)';
+ ctx.shadowBlur = 2;
+ ctx.shadowOffsetY = 1;
+
ctx.fillStyle = this.color;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
+
+ // Add shine effect
+ ctx.fillStyle = 'rgba(255,255,255,0.3)';
+ ctx.fillRect(-this.width / 2, -this.height / 2, this.width * 0.4, this.height);
+
ctx.restore();
};
/**
- * Create a firework explosion at position
+ * Create an enhanced firework explosion
*/
function createFirework(x, y) {
- var color = fireworkColors[Math.floor(Math.random() * fireworkColors.length)];
- var particleCount = 50 + Math.floor(Math.random() * 30);
+ var palette = fireworkPalettes[Math.floor(Math.random() * fireworkPalettes.length)];
+ var particleCount = 60 + Math.floor(Math.random() * 40);
+ var burstSpeed = 4 + Math.random() * 3;
+ // Main explosion particles
for (var i = 0; i < particleCount; i++) {
- var angle = (Math.PI * 2 / particleCount) * i;
- var speed = Math.random() * 5 + 3;
+ var angle = (Math.PI * 2 / particleCount) * i + (Math.random() - 0.5) * 0.2;
+ var speed = burstSpeed * (0.6 + Math.random() * 0.8);
+ var colorIndex = Math.floor(Math.random() * palette.length);
var velocity = {
x: Math.cos(angle) * speed,
y: Math.sin(angle) * speed
};
- particles.push(new Particle(x, y, color, velocity, 0.04, 0.012, Math.random() * 3 + 1));
+ particles.push(new Particle(x, y, palette[colorIndex], velocity, {
+ gravity: 0.05,
+ decay: 0.010 + Math.random() * 0.008,
+ size: 2 + Math.random() * 2,
+ trailLength: 4,
+ glowSize: 6
+ }));
+ }
+
+ // Inner ring (brighter, faster decay)
+ var innerCount = 20 + Math.floor(Math.random() * 15);
+ for (var j = 0; j < innerCount; j++) {
+ var innerAngle = Math.random() * Math.PI * 2;
+ var innerSpeed = burstSpeed * 1.3 * Math.random();
+ var innerVelocity = {
+ x: Math.cos(innerAngle) * innerSpeed,
+ y: Math.sin(innerAngle) * innerSpeed
+ };
+ particles.push(new Particle(x, y, '#FFFFFF', innerVelocity, {
+ gravity: 0.04,
+ decay: 0.025,
+ size: 1.5,
+ trailLength: 3,
+ glowSize: 4,
+ hasTrail: true
+ }));
}
- // Add some random sparkle particles
- for (var j = 0; j < 20; j++) {
+ // Sparkle particles (twinkle effect)
+ var sparkleCount = 15 + Math.floor(Math.random() * 10);
+ for (var k = 0; k < sparkleCount; k++) {
var sparkAngle = Math.random() * Math.PI * 2;
- var sparkSpeed = Math.random() * 8 + 2;
+ var sparkSpeed = burstSpeed * 0.3 + Math.random() * burstSpeed * 0.8;
var sparkVelocity = {
x: Math.cos(sparkAngle) * sparkSpeed,
y: Math.sin(sparkAngle) * sparkSpeed
};
- particles.push(new Particle(x, y, '#FFFFFF', sparkVelocity, 0.06, 0.025, 2));
+ particles.push(new Particle(x, y, '#FFFACD', sparkVelocity, {
+ gravity: 0.08,
+ decay: 0.03 + Math.random() * 0.02,
+ size: 1,
+ glowSize: 3,
+ hasTrail: false
+ }));
}
}
/**
- * Create confetti burst
+ * Create confetti burst from top
*/
function createConfettiBurst(count) {
if (!canvas) return;
@@ -137,7 +279,28 @@
for (var i = 0; i < count; i++) {
var x = Math.random() * canvas.width;
var color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
- confetti.push(new ConfettiPiece(x, -20, color));
+ confetti.push(new ConfettiPiece(x, -20 - Math.random() * 50, color));
+ }
+ }
+
+ /**
+ * Create confetti cannon from sides
+ */
+ function createConfettiCannon(fromLeft) {
+ if (!canvas) return;
+
+ var startX = fromLeft ? -10 : canvas.width + 10;
+ var angle = fromLeft ? -Math.PI / 4 : -Math.PI * 3 / 4;
+
+ for (var i = 0; i < 30; i++) {
+ var spread = (Math.random() - 0.5) * 0.5;
+ var speed = 8 + Math.random() * 6;
+ var color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
+ var piece = new ConfettiPiece(startX, canvas.height * 0.7, color);
+ piece.velocity.x = Math.cos(angle + spread) * speed;
+ piece.velocity.y = Math.sin(angle + spread) * speed;
+ piece.initialX = startX;
+ confetti.push(piece);
}
}
@@ -175,7 +338,9 @@
var elapsed = Date.now() - startTime;
- // Clear canvas
+ // Clear canvas with slight fade for trail effect
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Update and draw particles
@@ -192,20 +357,33 @@
return alive;
});
- // Create new fireworks periodically during first 2 seconds
- if (elapsed < 2000 && Math.random() < 0.08) {
- var x = Math.random() * canvas.width * 0.6 + canvas.width * 0.2;
- var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.1;
- createFirework(x, y);
+ // Create new fireworks periodically for 20 seconds
+ if (elapsed < fireworksDuration) {
+ // More frequent fireworks at start, gradually decreasing
+ var fireworkChance = elapsed < 5000 ? 0.08 : (elapsed < 10000 ? 0.05 : 0.03);
+ if (Math.random() < fireworkChance) {
+ var x = Math.random() * canvas.width * 0.7 + canvas.width * 0.15;
+ var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.08;
+ createFirework(x, y);
+ }
}
- // Add confetti continuously during first 3 seconds
- if (elapsed < 3000 && Math.random() < 0.3) {
- createConfettiBurst(3);
+ // Add confetti continuously forever (or until stopped)
+ if (confettiForever && Math.random() < 0.15) {
+ createConfettiBurst(1);
}
- // Continue animation until all particles are gone or duration exceeded
- if (elapsed < duration || particles.length > 0 || confetti.length > 0) {
+ // Confetti cannons at specific intervals during first 15 seconds
+ if (elapsed < 15000) {
+ var cannonInterval = 2000; // Every 2 seconds
+ var cannonWindow = 30;
+ if (elapsed % cannonInterval < cannonWindow && Math.random() < 0.5) {
+ createConfettiCannon(Math.random() < 0.5);
+ }
+ }
+
+ // Continue animation as long as running (confetti forever!)
+ if (isRunning) {
animationId = requestAnimationFrame(animate);
} else {
cleanup();
@@ -228,6 +406,7 @@
ctx = null;
particles = [];
confetti = [];
+ trails = [];
window.removeEventListener('resize', resizeCanvas);
}
@@ -242,19 +421,26 @@
startTime = Date.now();
particles = [];
confetti = [];
-
- // Initial burst of fireworks
- for (var i = 0; i < 5; i++) {
+ trails = [];
+
+ // Initial burst of fireworks (staggered)
+ var fireworkPositions = [
+ { x: 0.3, y: 0.25, delay: 0 },
+ { x: 0.7, y: 0.2, delay: 150 },
+ { x: 0.5, y: 0.15, delay: 300 },
+ { x: 0.25, y: 0.3, delay: 450 },
+ { x: 0.75, y: 0.28, delay: 600 }
+ ];
+
+ fireworkPositions.forEach(function(pos) {
setTimeout(function() {
- if (!isRunning) return;
- var x = Math.random() * canvas.width * 0.6 + canvas.width * 0.2;
- var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.15;
- createFirework(x, y);
- }, i * 200);
- }
+ if (!isRunning || !canvas) return;
+ createFirework(canvas.width * pos.x, canvas.height * pos.y);
+ }, pos.delay);
+ });
// Initial confetti burst
- createConfettiBurst(100);
+ createConfettiBurst(80);
animate();
console.log('Celebration started!');
diff --git a/assets/src/css/tailwind.css b/assets/src/css/tailwind.css
index 4c741fb..44ac9de 100644
--- a/assets/src/css/tailwind.css
+++ b/assets/src/css/tailwind.css
@@ -903,6 +903,34 @@
}
}
+/* Success stat cards with enhanced dark mode */
+.stat-card-success {
+ background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
+ border-radius: 0.75rem;
+ padding: 1rem;
+ border: 1px solid rgba(34, 197, 94, 0.2);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.stat-card-success:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(34, 197, 94, 0.15);
+}
+
+.dark .stat-card-success {
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.25) 100%);
+ border: 1px solid rgba(74, 222, 128, 0.3);
+ box-shadow: 0 0 20px rgba(34, 197, 94, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+.dark .stat-card-success:hover {
+ box-shadow: 0 0 25px rgba(34, 197, 94, 0.2),
+ 0 4px 15px rgba(0, 0, 0, 0.3),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
+ border-color: rgba(74, 222, 128, 0.5);
+}
+
/* ========================================
FILE UPLOAD ENHANCEMENTS
======================================== */
diff --git a/assets/src/js/celebration.js b/assets/src/js/celebration.js
index 7343c46..5d80a74 100644
--- a/assets/src/js/celebration.js
+++ b/assets/src/js/celebration.js
@@ -1,7 +1,7 @@
/**
- * BigDump - Celebration Effects Module
- * Creates fireworks and confetti animations on import completion.
- * Uses Canvas API for particle-based visual effects.
+ * BigDump - Celebration Effects Module (Enhanced)
+ * Creates realistic fireworks and confetti animations on import completion.
+ * Features: particle trails, glow effects, 3D confetti rotation, air resistance.
*/
(function() {
'use strict';
@@ -11,38 +11,63 @@
var animationId = null;
var particles = [];
var confetti = [];
+ var trails = [];
var isRunning = false;
var startTime = 0;
- var duration = 4000; // 4 seconds of celebration
-
- // Colors for fireworks and confetti
- var fireworkColors = [
- '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
- '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
+ var fireworksDuration = 20000; // 20 seconds of fireworks
+ var confettiForever = true; // Confetti falls indefinitely
+
+ // Vibrant colors for fireworks
+ var fireworkPalettes = [
+ ['#FF6B6B', '#FF8E8E', '#FFB4B4'], // Red
+ ['#4ECDC4', '#7EDCD6', '#A8EBE7'], // Teal
+ ['#45B7D1', '#6FC9DE', '#99DBEB'], // Blue
+ ['#FFD93D', '#FFE469', '#FFEF9A'], // Yellow
+ ['#6BCB77', '#8DD896', '#AFE5B5'], // Green
+ ['#FF8C00', '#FFA533', '#FFBE66'], // Orange
+ ['#DA70D6', '#E494E0', '#EEB8EA'], // Orchid
+ ['#9B59B6', '#B07CC6', '#C59FD6'], // Purple
+ ['#E74C3C', '#ED7669', '#F3A096'], // Coral
+ ['#3498DB', '#5DADE2', '#85C1E9'] // Sky blue
];
var confettiColors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77',
- '#FF8C00', '#DA70D6', '#20B2AA', '#FF69B4', '#87CEEB'
+ '#FF8C00', '#DA70D6', '#9B59B6', '#E74C3C', '#3498DB',
+ '#F39C12', '#1ABC9C', '#E91E63', '#00BCD4', '#8BC34A'
];
/**
- * Particle class for fireworks
+ * Enhanced Particle class with trails and glow
*/
- function Particle(x, y, color, velocity, gravity, decay, size) {
+ function Particle(x, y, color, velocity, options) {
+ options = options || {};
this.x = x;
this.y = y;
this.color = color;
this.velocity = velocity;
- this.gravity = gravity || 0.05;
- this.decay = decay || 0.015;
+ this.gravity = options.gravity || 0.06;
+ this.decay = options.decay || 0.015;
this.alpha = 1;
- this.size = size || 3;
+ this.size = options.size || 3;
+ this.hasTrail = options.hasTrail !== false;
+ this.glowSize = options.glowSize || this.size * 2;
+ this.friction = options.friction || 0.98;
+ this.trail = [];
+ this.maxTrailLength = options.trailLength || 5;
}
Particle.prototype.update = function() {
- this.velocity.x *= 0.99;
- this.velocity.y *= 0.99;
+ // Store trail position
+ if (this.hasTrail && this.alpha > 0.3) {
+ this.trail.push({ x: this.x, y: this.y, alpha: this.alpha });
+ if (this.trail.length > this.maxTrailLength) {
+ this.trail.shift();
+ }
+ }
+
+ this.velocity.x *= this.friction;
+ this.velocity.y *= this.friction;
this.velocity.y += this.gravity;
this.x += this.velocity.x;
this.y += this.velocity.y;
@@ -51,85 +76,202 @@
};
Particle.prototype.draw = function(ctx) {
+ // Draw trail
+ if (this.hasTrail && this.trail.length > 0) {
+ for (var i = 0; i < this.trail.length; i++) {
+ var t = this.trail[i];
+ var trailAlpha = (i / this.trail.length) * this.alpha * 0.5;
+ var trailSize = this.size * (0.3 + (i / this.trail.length) * 0.5);
+ ctx.save();
+ ctx.globalAlpha = trailAlpha;
+ ctx.beginPath();
+ ctx.arc(t.x, t.y, trailSize, 0, Math.PI * 2);
+ ctx.fillStyle = this.color;
+ ctx.fill();
+ ctx.restore();
+ }
+ }
+
+ // Draw glow
+ ctx.save();
+ ctx.globalAlpha = this.alpha * 0.4;
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2);
+ var gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.glowSize);
+ gradient.addColorStop(0, this.color);
+ gradient.addColorStop(1, 'transparent');
+ ctx.fillStyle = gradient;
+ ctx.fill();
+ ctx.restore();
+
+ // Draw core
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
+
+ // Bright center
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.size * 0.5, 0, Math.PI * 2);
+ ctx.fillStyle = '#FFFFFF';
+ ctx.globalAlpha = this.alpha * 0.8;
+ ctx.fill();
ctx.restore();
};
/**
- * Confetti piece class
+ * Enhanced Confetti with 3D rotation and air resistance
*/
function ConfettiPiece(x, y, color) {
this.x = x;
this.y = y;
this.color = color;
- this.width = Math.random() * 10 + 5;
- this.height = Math.random() * 6 + 3;
+ this.width = Math.random() * 12 + 6;
+ this.height = Math.random() * 8 + 4;
this.velocity = {
- x: (Math.random() - 0.5) * 6,
- y: Math.random() * 3 + 2
+ x: (Math.random() - 0.5) * 8,
+ y: Math.random() * 2 + 1
};
- this.rotation = Math.random() * 360;
- this.rotationSpeed = (Math.random() - 0.5) * 10;
- this.oscillationSpeed = Math.random() * 0.1 + 0.05;
- this.oscillationDistance = Math.random() * 40 + 20;
+ // 3D rotation angles
+ this.rotationX = Math.random() * 360;
+ this.rotationY = Math.random() * 360;
+ this.rotationZ = Math.random() * 360;
+ this.rotationSpeedX = (Math.random() - 0.5) * 8;
+ this.rotationSpeedY = (Math.random() - 0.5) * 12;
+ this.rotationSpeedZ = (Math.random() - 0.5) * 6;
+ // Oscillation (swaying)
+ this.oscillationSpeed = Math.random() * 0.08 + 0.03;
+ this.oscillationDistance = Math.random() * 60 + 30;
this.initialX = x;
this.time = Math.random() * 100;
+ // Air resistance
+ this.drag = 0.98 + Math.random() * 0.015;
+ this.terminalVelocity = 4 + Math.random() * 2;
}
ConfettiPiece.prototype.update = function(canvasHeight) {
this.time += this.oscillationSpeed;
+
+ // Horizontal oscillation (swaying in wind)
this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance;
+ this.initialX += this.velocity.x * 0.1; // Drift
+
+ // Apply drag
+ this.velocity.x *= this.drag;
+ this.velocity.y += 0.08; // Gravity
+
+ // Terminal velocity
+ if (this.velocity.y > this.terminalVelocity) {
+ this.velocity.y = this.terminalVelocity;
+ }
+
this.y += this.velocity.y;
- this.rotation += this.rotationSpeed;
- this.velocity.y += 0.02; // Light gravity
+
+ // Tumbling rotation
+ this.rotationX += this.rotationSpeedX;
+ this.rotationY += this.rotationSpeedY;
+ this.rotationZ += this.rotationSpeedZ;
+
+ // Slow down rotation over time
+ this.rotationSpeedX *= 0.999;
+ this.rotationSpeedY *= 0.999;
+
return this.y < canvasHeight + 50;
};
ConfettiPiece.prototype.draw = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
- ctx.rotate(this.rotation * Math.PI / 180);
+
+ // Apply 3D-like rotation (fake perspective)
+ var scaleX = Math.cos(this.rotationY * Math.PI / 180);
+ var scaleY = Math.cos(this.rotationX * Math.PI / 180);
+ ctx.rotate(this.rotationZ * Math.PI / 180);
+ ctx.scale(scaleX, scaleY);
+
+ // Draw confetti piece with slight shadow
+ ctx.shadowColor = 'rgba(0,0,0,0.2)';
+ ctx.shadowBlur = 2;
+ ctx.shadowOffsetY = 1;
+
ctx.fillStyle = this.color;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
+
+ // Add shine effect
+ ctx.fillStyle = 'rgba(255,255,255,0.3)';
+ ctx.fillRect(-this.width / 2, -this.height / 2, this.width * 0.4, this.height);
+
ctx.restore();
};
/**
- * Create a firework explosion at position
+ * Create an enhanced firework explosion
*/
function createFirework(x, y) {
- var color = fireworkColors[Math.floor(Math.random() * fireworkColors.length)];
- var particleCount = 50 + Math.floor(Math.random() * 30);
+ var palette = fireworkPalettes[Math.floor(Math.random() * fireworkPalettes.length)];
+ var particleCount = 60 + Math.floor(Math.random() * 40);
+ var burstSpeed = 4 + Math.random() * 3;
+ // Main explosion particles
for (var i = 0; i < particleCount; i++) {
- var angle = (Math.PI * 2 / particleCount) * i;
- var speed = Math.random() * 5 + 3;
+ var angle = (Math.PI * 2 / particleCount) * i + (Math.random() - 0.5) * 0.2;
+ var speed = burstSpeed * (0.6 + Math.random() * 0.8);
+ var colorIndex = Math.floor(Math.random() * palette.length);
var velocity = {
x: Math.cos(angle) * speed,
y: Math.sin(angle) * speed
};
- particles.push(new Particle(x, y, color, velocity, 0.04, 0.012, Math.random() * 3 + 1));
+ particles.push(new Particle(x, y, palette[colorIndex], velocity, {
+ gravity: 0.05,
+ decay: 0.010 + Math.random() * 0.008,
+ size: 2 + Math.random() * 2,
+ trailLength: 4,
+ glowSize: 6
+ }));
+ }
+
+ // Inner ring (brighter, faster decay)
+ var innerCount = 20 + Math.floor(Math.random() * 15);
+ for (var j = 0; j < innerCount; j++) {
+ var innerAngle = Math.random() * Math.PI * 2;
+ var innerSpeed = burstSpeed * 1.3 * Math.random();
+ var innerVelocity = {
+ x: Math.cos(innerAngle) * innerSpeed,
+ y: Math.sin(innerAngle) * innerSpeed
+ };
+ particles.push(new Particle(x, y, '#FFFFFF', innerVelocity, {
+ gravity: 0.04,
+ decay: 0.025,
+ size: 1.5,
+ trailLength: 3,
+ glowSize: 4,
+ hasTrail: true
+ }));
}
- // Add some random sparkle particles
- for (var j = 0; j < 20; j++) {
+ // Sparkle particles (twinkle effect)
+ var sparkleCount = 15 + Math.floor(Math.random() * 10);
+ for (var k = 0; k < sparkleCount; k++) {
var sparkAngle = Math.random() * Math.PI * 2;
- var sparkSpeed = Math.random() * 8 + 2;
+ var sparkSpeed = burstSpeed * 0.3 + Math.random() * burstSpeed * 0.8;
var sparkVelocity = {
x: Math.cos(sparkAngle) * sparkSpeed,
y: Math.sin(sparkAngle) * sparkSpeed
};
- particles.push(new Particle(x, y, '#FFFFFF', sparkVelocity, 0.06, 0.025, 2));
+ particles.push(new Particle(x, y, '#FFFACD', sparkVelocity, {
+ gravity: 0.08,
+ decay: 0.03 + Math.random() * 0.02,
+ size: 1,
+ glowSize: 3,
+ hasTrail: false
+ }));
}
}
/**
- * Create confetti burst
+ * Create confetti burst from top
*/
function createConfettiBurst(count) {
if (!canvas) return;
@@ -137,7 +279,28 @@
for (var i = 0; i < count; i++) {
var x = Math.random() * canvas.width;
var color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
- confetti.push(new ConfettiPiece(x, -20, color));
+ confetti.push(new ConfettiPiece(x, -20 - Math.random() * 50, color));
+ }
+ }
+
+ /**
+ * Create confetti cannon from sides
+ */
+ function createConfettiCannon(fromLeft) {
+ if (!canvas) return;
+
+ var startX = fromLeft ? -10 : canvas.width + 10;
+ var angle = fromLeft ? -Math.PI / 4 : -Math.PI * 3 / 4;
+
+ for (var i = 0; i < 30; i++) {
+ var spread = (Math.random() - 0.5) * 0.5;
+ var speed = 8 + Math.random() * 6;
+ var color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
+ var piece = new ConfettiPiece(startX, canvas.height * 0.7, color);
+ piece.velocity.x = Math.cos(angle + spread) * speed;
+ piece.velocity.y = Math.sin(angle + spread) * speed;
+ piece.initialX = startX;
+ confetti.push(piece);
}
}
@@ -175,7 +338,9 @@
var elapsed = Date.now() - startTime;
- // Clear canvas
+ // Clear canvas with slight fade for trail effect
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Update and draw particles
@@ -192,20 +357,33 @@
return alive;
});
- // Create new fireworks periodically during first 2 seconds
- if (elapsed < 2000 && Math.random() < 0.08) {
- var x = Math.random() * canvas.width * 0.6 + canvas.width * 0.2;
- var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.1;
- createFirework(x, y);
+ // Create new fireworks periodically for 20 seconds
+ if (elapsed < fireworksDuration) {
+ // More frequent fireworks at start, gradually decreasing
+ var fireworkChance = elapsed < 5000 ? 0.08 : (elapsed < 10000 ? 0.05 : 0.03);
+ if (Math.random() < fireworkChance) {
+ var x = Math.random() * canvas.width * 0.7 + canvas.width * 0.15;
+ var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.08;
+ createFirework(x, y);
+ }
}
- // Add confetti continuously during first 3 seconds
- if (elapsed < 3000 && Math.random() < 0.3) {
- createConfettiBurst(3);
+ // Add confetti continuously forever (or until stopped)
+ if (confettiForever && Math.random() < 0.15) {
+ createConfettiBurst(1);
}
- // Continue animation until all particles are gone or duration exceeded
- if (elapsed < duration || particles.length > 0 || confetti.length > 0) {
+ // Confetti cannons at specific intervals during first 15 seconds
+ if (elapsed < 15000) {
+ var cannonInterval = 2000; // Every 2 seconds
+ var cannonWindow = 30;
+ if (elapsed % cannonInterval < cannonWindow && Math.random() < 0.5) {
+ createConfettiCannon(Math.random() < 0.5);
+ }
+ }
+
+ // Continue animation as long as running (confetti forever!)
+ if (isRunning) {
animationId = requestAnimationFrame(animate);
} else {
cleanup();
@@ -228,6 +406,7 @@
ctx = null;
particles = [];
confetti = [];
+ trails = [];
window.removeEventListener('resize', resizeCanvas);
}
@@ -242,19 +421,26 @@
startTime = Date.now();
particles = [];
confetti = [];
-
- // Initial burst of fireworks
- for (var i = 0; i < 5; i++) {
+ trails = [];
+
+ // Initial burst of fireworks (staggered)
+ var fireworkPositions = [
+ { x: 0.3, y: 0.25, delay: 0 },
+ { x: 0.7, y: 0.2, delay: 150 },
+ { x: 0.5, y: 0.15, delay: 300 },
+ { x: 0.25, y: 0.3, delay: 450 },
+ { x: 0.75, y: 0.28, delay: 600 }
+ ];
+
+ fireworkPositions.forEach(function(pos) {
setTimeout(function() {
- if (!isRunning) return;
- var x = Math.random() * canvas.width * 0.6 + canvas.width * 0.2;
- var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.15;
- createFirework(x, y);
- }, i * 200);
- }
+ if (!isRunning || !canvas) return;
+ createFirework(canvas.width * pos.x, canvas.height * pos.y);
+ }, pos.delay);
+ });
// Initial confetti burst
- createConfettiBurst(100);
+ createConfettiBurst(80);
animate();
console.log('Celebration started!');
diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php
index f968ca4..b2cee2b 100644
--- a/src/Services/AjaxService.php
+++ b/src/Services/AjaxService.php
@@ -191,21 +191,21 @@ function displaySuccessInPage(stats) {
'