* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.counter-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.counter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
max-width: 1200px;
width: 100%;
margin-bottom: 3rem;
}
.counter-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 2rem;
text-align: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.counter-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.5s ease;
}
.counter-card:hover::before {
left: 100%;
}
.counter-card:hover {
transform: translateY(-10px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
.counter-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.counter-number {
font-size: 3rem;
font-weight: 700;
color: white;
margin: 1rem 0;
display: inline-block;
position: relative;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.counter-prefix {
font-size: 2rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.8);
display: inline-block;
vertical-align: top;
margin-right: 0.2rem;
margin-top: 0.5rem;
}
.counter-suffix {
font-size: 2rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.8);
display: inline-block;
vertical-align: top;
margin-left: 0.2rem;
margin-top: 0.5rem;
}
.counter-label {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.counter-number.animating {
color: #ffd700;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
.counter-controls {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
}
.control-btn {
padding: 1rem 2rem;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50px;
color: white;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.control-btn:active {
transform: translateY(0);
}
/* Animación de Anillo de Progreso */
.counter-card.counting {
position: relative;
}
.counter-card.counting::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 120px;
height: 120px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top: 3px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
transform: translate(-50%, -50%);
animation: spin 2s linear infinite;
pointer-events: none;
}
@keyframes spin {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
/* Efecto de Pulso para Animación Completada */
.counter-number.completed {
animation: pulse 0.6s ease-in-out;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
color: #ffd700;
text-shadow: 0 0 30px rgba(255, 215, 0, 0.8);
}
100% {
transform: scale(1);
}
}
/* Diseño Responsivo */
@media (max-width: 768px) {
.counter-container {
padding: 1rem;
}
.counter-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.counter-card {
padding: 1.5rem;
}
.counter-number {
font-size: 2.5rem;
}
.counter-icon {
font-size: 2.5rem;
}
.control-btn {
padding: 0.8rem 1.5rem;
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
.counter-grid {
grid-template-columns: 1fr;
}
.counter-number {
font-size: 2rem;
}
.counter-icon {
font-size: 2rem;
}
}
/* Soporte para Modo Oscuro */
@media (prefers-color-scheme: dark) {
.counter-card {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.1);
}
}
class AnimatedCounter {
constructor() {
this.counters = document.querySelectorAll('.counter-number');
this.startBtn = document.getElementById('startBtn');
this.resetBtn = document.getElementById('resetBtn');
this.observer = null;
this.hasAnimated = false;
this.init();
}
init() {
this.setupEventListeners();
this.setupIntersectionObserver();
}
setupEventListeners() {
this.startBtn.addEventListener('click', () => {
this.startAllCounters();
});
this.resetBtn.addEventListener('click', () => {
this.resetAllCounters();
});
}
setupIntersectionObserver() {
// Auto-iniciar animación cuando los contadores entran en vista
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !this.hasAnimated) {
setTimeout(() => {
this.startAllCounters();
this.hasAnimated = true;
}, 500);
}
});
}, {
threshold: 0.5
});
const counterGrid = document.querySelector('.counter-grid');
if (counterGrid) {
this.observer.observe(counterGrid);
}
}
startAllCounters() {
this.counters.forEach((counter, index) => {
setTimeout(() => {
this.animateCounter(counter);
}, index * 200); // Escalonar las animaciones
});
}
resetAllCounters() {
this.counters.forEach(counter => {
this.resetCounter(counter);
});
this.hasAnimated = false;
}
animateCounter(counter) {
const target = parseFloat(counter.getAttribute('data-target'));
const duration = parseInt(counter.getAttribute('data-duration')) || 2000;
const decimals = parseInt(counter.getAttribute('data-decimals')) || 0;
const format = counter.getAttribute('data-format') || 'number';
const card = counter.closest('.counter-card');
// Agregar clases de animación
counter.classList.add('animating');
card.classList.add('counting');
let startTime = null;
const startValue = 0;
const animate = (currentTime) => {
if (startTime === null) startTime = currentTime;
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Función de easing (ease-out cubic)
const easedProgress = 1 - Math.pow(1 - progress, 3);
const currentValue = startValue + (target - startValue) * easedProgress;
// Formatear y mostrar el número
counter.textContent = this.formatNumber(currentValue, format, decimals);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// Animación completada
counter.textContent = this.formatNumber(target, format, decimals);
counter.classList.remove('animating');
counter.classList.add('completed');
card.classList.remove('counting');
// Remover clase completed después de la animación
setTimeout(() => {
counter.classList.remove('completed');
}, 600);
}
};
requestAnimationFrame(animate);
}
resetCounter(counter) {
const card = counter.closest('.counter-card');
counter.classList.remove('animating', 'completed');
card.classList.remove('counting');
counter.textContent = '0';
}
formatNumber(value, format, decimals) {
switch (format) {
case 'currency':
return this.formatCurrency(value, decimals);
case 'compact':
return this.formatCompact(value, decimals);
case 'percentage':
return this.formatPercentage(value, decimals);
default:
return this.formatRegular(value, decimals);
}
}
formatRegular(value, decimals) {
return value.toLocaleString('es-ES', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
}
formatCurrency(value, decimals) {
if (value >= 1000000) {
return (value / 1000000).toFixed(decimals) + 'M';
} else if (value >= 1000) {
return (value / 1000).toFixed(decimals) + 'K';
}
return value.toLocaleString('es-ES', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
}
formatCompact(value, decimals) {
if (value >= 1000000) {
return (value / 1000000).toFixed(decimals) + 'M';
} else if (value >= 1000) {
return (value / 1000).toFixed(decimals) + 'K';
}
return Math.floor(value).toLocaleString('es-ES');
}
formatPercentage(value, decimals) {
return value.toFixed(decimals);
}
// Métodos públicos para control externo
startCounter(index) {
if (this.counters[index]) {
this.animateCounter(this.counters[index]);
}
}
resetCounter(index) {
if (this.counters[index]) {
this.resetCounter(this.counters[index]);
}
}
// Agregar contador personalizado programáticamente
addCounter(element, target, duration = 2000, options = {}) {
element.setAttribute('data-target', target);
element.setAttribute('data-duration', duration);
if (options.decimals !== undefined) {
element.setAttribute('data-decimals', options.decimals);
}
if (options.format) {
element.setAttribute('data-format', options.format);
}
this.animateCounter(element);
}
// Monitoreo de rendimiento
getPerformanceMetrics() {
return {
totalCounters: this.counters.length,
hasAnimated: this.hasAnimated,
observerActive: this.observer !== null
};
}
// Método de limpieza
destroy() {
if (this.observer) {
this.observer.disconnect();
}
this.startBtn.removeEventListener('click', this.startAllCounters);
this.resetBtn.removeEventListener('click', this.resetAllCounters);
}
}
// Función utilitaria para crear contadores dinámicamente
function createCounter(container, config) {
const counterCard = document.createElement('div');
counterCard.className = 'counter-card';
counterCard.innerHTML = `
<div class="counter-icon">${config.icon || '📊'}</div>
${config.prefix ? `<div class="counter-prefix">${config.prefix}</div>` : ''}
<div class="counter-number"
data-target="${config.target}"
data-duration="${config.duration || 2000}"
${config.decimals !== undefined ? `data-decimals="${config.decimals}"` : ''}
${config.format ? `data-format="${config.format}"` : ''}>
0
</div>
<div class="counter-label">${config.label}</div>
${config.suffix ? `<div class="counter-suffix">${config.suffix}</div>` : ''}
`;
container.appendChild(counterCard);
return counterCard.querySelector('.counter-number');
}
// Inicializar el sistema de contadores
document.addEventListener('DOMContentLoaded', () => {
window.animatedCounter = new AnimatedCounter();
// Ejemplo de agregar un contador personalizado
// const grid = document.querySelector('.counter-grid');
// const customCounter = createCounter(grid, {
// icon: '🚀',
// target: 999,
// duration: 3000,
// label: 'Contador Personalizado',
// suffix: '+'
// });
});