Загрузка...

Анимированный счетчик очков: крупный дисплей с плавной анимацией и сменой цветов. Отображает динамический заголовок в зависимости от диапазона.
<div class="w-full max-w-md">
<style>@import url('https://api.fontshare.com/v2/css?f[]=satoshi@300,400,500,700,900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
.score-counter-comp {
font-family: 'Satoshi', 'Helvetica Neue', sans-serif;
}
.score-counter-comp .font-mono {
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
@keyframes gradientSlide {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
.score-counter-comp .score-viral {
background: linear-gradient(90deg, #D97706, #F59E0B, #FCD34D, #F59E0B, #D97706);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradientSlide 3s linear infinite;
}
.score-counter-comp .score-glow {
filter: drop-shadow(0 0 20px rgba(217, 119, 6, 0.4)) drop-shadow(0 0 40px rgba(217, 119, 6, 0.2));
}</style>
<div class="score-counter-comp w-full text-center">
<div class="flex items-baseline justify-center gap-3">
<span id="score-counter-number" class="score-number text-7xl font-bold font-mono tracking-tighter transition-colors duration-100" style="color: #EF4444;">0</span>
<span class="text-sm text-neutral-500 font-medium">/100</span>
</div>
<span id="score-counter-label" class="text-xs uppercase tracking-widest font-medium mt-3
inline-block opacity-0 transform translate-y-2
transition-all duration-300" style="color: #EF4444;">Calculating...</span>
</div>
<script>(function() {
const TARGET_SCORE = 96;
const ANIMATION_DURATION = 1500;
const SCORE_RANGES = [
{ min: 0, max: 30, color: '#EF4444', label: 'NEEDS WORK' },
{ min: 31, max: 60, color: '#EAB308', label: 'AVERAGE' },
{ min: 61, max: 80, color: '#22C55E', label: 'GOOD' },
{ min: 81, max: 90, color: '#14B8A6', label: 'EXCELLENT' },
{ min: 91, max: 100, color: '#D97706', label: 'VIRAL READY' }
];
function getScoreConfig(score) {
return SCORE_RANGES.find(r => score >= r.min && score <= r.max) || SCORE_RANGES[0];
}
function easeOutExpo(x) {
return x === 1 ? 1 : 1 - Math.pow(2, -10 * x);
}
function animateScore(targetScore) {
const scoreElement = document.getElementById('score-counter-number');
const labelElement = document.getElementById('score-counter-label');
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / ANIMATION_DURATION, 1);
const easedProgress = easeOutExpo(progress);
const currentScore = Math.floor(easedProgress * targetScore);
scoreElement.textContent = currentScore;
const config = getScoreConfig(currentScore);
if (currentScore >= 91) {
scoreElement.classList.add('score-viral', 'score-glow');
scoreElement.style.color = '';
} else {
scoreElement.classList.remove('score-viral', 'score-glow');
scoreElement.style.color = config.color;
}
if (progress < 1) {
requestAnimationFrame(update);
} else {
scoreElement.textContent = targetScore;
const finalConfig = getScoreConfig(targetScore);
labelElement.textContent = finalConfig.label;
labelElement.style.color = finalConfig.color;
labelElement.style.opacity = '1';
labelElement.style.transform = 'translateY(0)';
if (targetScore >= 91) {
scoreElement.classList.add('score-viral', 'score-glow');
}
}
}
requestAnimationFrame(update);
}
const scoreCounter = document.getElementById('score-counter-number');
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.3) {
setTimeout(() => animateScore(TARGET_SCORE), 200);
observer.unobserve(entry.target);
}
});
}, {
root: null,
rootMargin: '0px',
threshold: 0.3
});
observer.observe(scoreCounter);
} else {
setTimeout(() => animateScore(TARGET_SCORE), 500);
}
})();</script>
</div>