| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function reducedMotion() { |
| 5 |
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
| 6 |
} |
| 7 |
|
| 8 |
// angle = 0 → left (min), angle = 180 → right (max) |
| 9 |
function polarToCartesian(cx, cy, r, angleDeg) { |
| 10 |
var rad = (angleDeg * Math.PI) / 180; |
| 11 |
return { |
| 12 |
x: cx - r * Math.cos(rad), |
| 13 |
y: cy - r * Math.sin(rad) |
| 14 |
}; |
| 15 |
} |
| 16 |
|
| 17 |
// Ease-out cubic |
| 18 |
function easeOut(t) { |
| 19 |
return 1 - Math.pow(1 - t, 3); |
| 20 |
} |
| 21 |
|
| 22 |
function animateGauge(wrap) { |
| 23 |
var value = parseFloat(wrap.dataset.value) || 0; |
| 24 |
var minValue = parseFloat(wrap.dataset.min) || 0; |
| 25 |
var maxValue = parseFloat(wrap.dataset.max) || 100; |
| 26 |
var duration = parseFloat(wrap.dataset.duration) || 1200; |
| 27 |
var perimeter = parseFloat(wrap.dataset.perimeter) || 0; |
| 28 |
var unit = wrap.dataset.unit || ''; |
| 29 |
var showNeedle= wrap.dataset.showNeedle === '1'; |
| 30 |
var r = parseFloat(wrap.dataset.r) || 0; |
| 31 |
var cx = parseFloat(wrap.dataset.cx) || 0; |
| 32 |
var cy = parseFloat(wrap.dataset.cy) || 0; |
| 33 |
|
| 34 |
var fraction = Math.min(1, Math.max(0, (value - minValue) / (maxValue - minValue))); |
| 35 |
var finalOffset = (1 - fraction) * perimeter; |
| 36 |
|
| 37 |
var fillArc = wrap.querySelector('.bkgc-fill'); |
| 38 |
var valueText = wrap.querySelector('.bkgc-value-text'); |
| 39 |
var needle = wrap.querySelector('.bkgc-needle'); |
| 40 |
|
| 41 |
if (!fillArc && !valueText && !needle) return; |
| 42 |
|
| 43 |
// Needle tip radius (derived from geometry stored in data attributes) |
| 44 |
// r = cx - sw * 1.5 - 2 → sw = (cx - r - 2) / 1.5 |
| 45 |
var sw = (cx - r - 2) / 1.5; |
| 46 |
var needleTipR = r - sw - 4; |
| 47 |
|
| 48 |
if (reducedMotion()) { |
| 49 |
if (fillArc) fillArc.style.strokeDashoffset = finalOffset; |
| 50 |
if (valueText) valueText.textContent = Math.round(value) + unit; |
| 51 |
if (needle && showNeedle) { |
| 52 |
var tip = polarToCartesian(cx, cy, needleTipR, fraction * 180); |
| 53 |
needle.setAttribute('x2', tip.x); |
| 54 |
needle.setAttribute('y2', tip.y); |
| 55 |
} |
| 56 |
return; |
| 57 |
} |
| 58 |
|
| 59 |
var startTime = null; |
| 60 |
|
| 61 |
function step(ts) { |
| 62 |
if (!startTime) startTime = ts; |
| 63 |
var elapsed = ts - startTime; |
| 64 |
var t = Math.min(1, elapsed / duration); |
| 65 |
var eased = easeOut(t); |
| 66 |
|
| 67 |
// Arc fill — stroke-dashoffset from perimeter (empty) to finalOffset |
| 68 |
if (fillArc) { |
| 69 |
fillArc.style.strokeDashoffset = perimeter - eased * (perimeter - finalOffset); |
| 70 |
} |
| 71 |
|
| 72 |
// Count-up value text |
| 73 |
if (valueText) { |
| 74 |
var displayed = minValue + eased * (value - minValue); |
| 75 |
valueText.textContent = Math.round(displayed) + unit; |
| 76 |
} |
| 77 |
|
| 78 |
// Needle sweep from angle=0 (left/min) to fraction*180 |
| 79 |
if (needle && showNeedle) { |
| 80 |
var currentAngle = eased * fraction * 180; |
| 81 |
var nTip = polarToCartesian(cx, cy, needleTipR, currentAngle); |
| 82 |
needle.setAttribute('x2', nTip.x); |
| 83 |
needle.setAttribute('y2', nTip.y); |
| 84 |
} |
| 85 |
|
| 86 |
if (t < 1) { |
| 87 |
requestAnimationFrame(step); |
| 88 |
} |
| 89 |
} |
| 90 |
|
| 91 |
requestAnimationFrame(step); |
| 92 |
} |
| 93 |
|
| 94 |
function initGauge(wrap) { |
| 95 |
if (typeof IntersectionObserver === 'undefined') { |
| 96 |
animateGauge(wrap); |
| 97 |
return; |
| 98 |
} |
| 99 |
|
| 100 |
var triggered = false; |
| 101 |
|
| 102 |
var observer = new IntersectionObserver(function (entries) { |
| 103 |
entries.forEach(function (entry) { |
| 104 |
if (entry.isIntersecting && !triggered) { |
| 105 |
triggered = true; |
| 106 |
observer.unobserve(wrap); |
| 107 |
animateGauge(wrap); |
| 108 |
} |
| 109 |
}); |
| 110 |
}, { threshold: 0.25 }); |
| 111 |
|
| 112 |
observer.observe(wrap); |
| 113 |
} |
| 114 |
|
| 115 |
function init() { |
| 116 |
document.querySelectorAll('.bkgc-wrap[data-value]').forEach(initGauge); |
| 117 |
} |
| 118 |
|
| 119 |
if (document.readyState === 'loading') { |
| 120 |
document.addEventListener('DOMContentLoaded', init); |
| 121 |
} else { |
| 122 |
init(); |
| 123 |
} |
| 124 |
|
| 125 |
}()); |
| 126 |
|