| 1 |
/* Progress Circle — frontend */ |
| 2 |
(function () { |
| 3 |
'use strict'; |
| 4 |
|
| 5 |
var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
| 6 |
|
| 7 |
/* ── ease helpers ────────────────────────────────────────────────────────── */ |
| 8 |
function easeInOut(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; } |
| 9 |
function easeOut(t) { return 1 - Math.pow(1 - t, 3); } |
| 10 |
function easeIn(t) { return t * t * t; } |
| 11 |
function linear(t) { return t; } |
| 12 |
|
| 13 |
function getEaseFn(name) { |
| 14 |
if (name === 'ease-out') return easeOut; |
| 15 |
if (name === 'ease-in') return easeIn; |
| 16 |
if (name === 'linear') return linear; |
| 17 |
return easeInOut; /* ease-in-out default */ |
| 18 |
} |
| 19 |
|
| 20 |
/* ── animate one ring ───────────────────────────────────────────────────── */ |
| 21 |
function animateRing(ring, circumference, fromOffset, toOffset, duration, easeFn, onProgress) { |
| 22 |
if (reducedMotion) { |
| 23 |
ring.style.strokeDashoffset = toOffset; |
| 24 |
if (onProgress) { onProgress(1); } |
| 25 |
return; |
| 26 |
} |
| 27 |
var start = null; |
| 28 |
function step(ts) { |
| 29 |
if (!start) { start = ts; } |
| 30 |
var elapsed = Math.min(ts - start, duration); |
| 31 |
var t = easeFn(elapsed / duration); |
| 32 |
ring.style.strokeDashoffset = fromOffset + (toOffset - fromOffset) * t; |
| 33 |
if (onProgress) { onProgress(t); } |
| 34 |
if (elapsed < duration) { requestAnimationFrame(step); } |
| 35 |
else { ring.style.strokeDashoffset = toOffset; if (onProgress) { onProgress(1); } } |
| 36 |
} |
| 37 |
requestAnimationFrame(step); |
| 38 |
} |
| 39 |
|
| 40 |
/* ── animate counter text ───────────────────────────────────────────────── */ |
| 41 |
function animateCounter(el, from, to, suffix, duration, easeFn) { |
| 42 |
if (reducedMotion) { el.textContent = to + suffix; return; } |
| 43 |
var start = null; |
| 44 |
function step(ts) { |
| 45 |
if (!start) { start = ts; } |
| 46 |
var elapsed = Math.min(ts - start, duration); |
| 47 |
var t = easeFn(elapsed / duration); |
| 48 |
el.textContent = Math.round(from + (to - from) * t) + suffix; |
| 49 |
if (elapsed < duration) { requestAnimationFrame(step); } |
| 50 |
else { el.textContent = to + suffix; } |
| 51 |
} |
| 52 |
requestAnimationFrame(step); |
| 53 |
} |
| 54 |
|
| 55 |
/* ── init one item ───────────────────────────────────────────────────────── */ |
| 56 |
function initItem(item, grid) { |
| 57 |
if (item._bkpcInit) { return; } |
| 58 |
item._bkpcInit = true; |
| 59 |
|
| 60 |
var pct = parseFloat(item.getAttribute('data-pct')) || 0; |
| 61 |
var size = parseFloat(item.getAttribute('data-size')) || 140; |
| 62 |
var sw = parseFloat(item.getAttribute('data-sw')) || 10; |
| 63 |
var color = item.getAttribute('data-color') || '#6c3fb5'; |
| 64 |
var track = item.getAttribute('data-track') || '#e5e7eb'; |
| 65 |
var linecap = item.getAttribute('data-linecap') || 'round'; |
| 66 |
var gradient = item.getAttribute('data-gradient') === '1'; |
| 67 |
var gradEnd = item.getAttribute('data-grad-end') || '#ec4899'; |
| 68 |
var shadow = item.getAttribute('data-shadow') === '1'; |
| 69 |
|
| 70 |
var animate = grid.getAttribute('data-animate') !== '0'; |
| 71 |
var duration = parseInt(grid.getAttribute('data-duration'), 10) || 1200; |
| 72 |
var easing = grid.getAttribute('data-easing') || 'ease-in-out'; |
| 73 |
var counter = grid.getAttribute('data-counter') !== '0'; |
| 74 |
var suffix = grid.getAttribute('data-suffix') || '%'; |
| 75 |
|
| 76 |
var easeFn = getEaseFn(easing); |
| 77 |
var r = (size - sw) / 2; |
| 78 |
var circ = 2 * Math.PI * r; |
| 79 |
var ring = item.querySelector('.bkpc-ring'); |
| 80 |
if (!ring) { return; } |
| 81 |
|
| 82 |
/* patch gradient defs if needed */ |
| 83 |
if (gradient) { |
| 84 |
var svg = item.querySelector('svg'); |
| 85 |
if (svg) { |
| 86 |
var uid = 'bkpc-g-fe-' + Math.random().toString(36).slice(2, 7); |
| 87 |
var defs = svg.querySelector('defs'); |
| 88 |
if (defs) { |
| 89 |
var lg = defs.querySelector('linearGradient'); |
| 90 |
if (lg) { |
| 91 |
lg.id = uid; |
| 92 |
ring.style.stroke = 'url(#' + uid + ')'; |
| 93 |
ring.setAttribute('stroke', 'url(#' + uid + ')'); |
| 94 |
} |
| 95 |
} |
| 96 |
} |
| 97 |
} |
| 98 |
|
| 99 |
var targetOffset = circ - circ * (pct / 100); |
| 100 |
|
| 101 |
if (animate) { |
| 102 |
/* start rings at full offset (empty) */ |
| 103 |
ring.style.strokeDashoffset = circ; |
| 104 |
|
| 105 |
/* counter element */ |
| 106 |
var pctEl = item.querySelector('.bkpc-pct, .bkpc-pct-inner'); |
| 107 |
|
| 108 |
animateRing(ring, circ, circ, targetOffset, duration, easeFn, null); |
| 109 |
if (counter && pctEl) { |
| 110 |
animateCounter(pctEl, 0, pct, suffix, duration, easeFn); |
| 111 |
} |
| 112 |
} else { |
| 113 |
ring.style.strokeDashoffset = targetOffset; |
| 114 |
} |
| 115 |
} |
| 116 |
|
| 117 |
/* ── init one grid ───────────────────────────────────────────────────────── */ |
| 118 |
function initGrid(grid) { |
| 119 |
if (grid._bkpcInit) { return; } |
| 120 |
grid._bkpcInit = true; |
| 121 |
|
| 122 |
var animate = grid.getAttribute('data-animate') !== '0'; |
| 123 |
var items = Array.prototype.slice.call(grid.querySelectorAll('.bkpc-item[data-pct]')); |
| 124 |
|
| 125 |
if (!animate || reducedMotion) { |
| 126 |
items.forEach(function (item) { initItem(item, grid); }); |
| 127 |
return; |
| 128 |
} |
| 129 |
|
| 130 |
/* observe the grid */ |
| 131 |
if ('IntersectionObserver' in window) { |
| 132 |
var obs = new IntersectionObserver(function (entries) { |
| 133 |
entries.forEach(function (entry) { |
| 134 |
if (entry.isIntersecting) { |
| 135 |
items.forEach(function (item) { initItem(item, grid); }); |
| 136 |
obs.disconnect(); |
| 137 |
} |
| 138 |
}); |
| 139 |
}, { threshold: 0.2 }); |
| 140 |
obs.observe(grid); |
| 141 |
} else { |
| 142 |
items.forEach(function (item) { initItem(item, grid); }); |
| 143 |
} |
| 144 |
} |
| 145 |
|
| 146 |
/* ── boot ────────────────────────────────────────────────────────────────── */ |
| 147 |
function init() { |
| 148 |
document.querySelectorAll('.bkpc-grid[data-animate]').forEach(initGrid); |
| 149 |
} |
| 150 |
|
| 151 |
if (document.readyState === 'loading') { |
| 152 |
document.addEventListener('DOMContentLoaded', init); |
| 153 |
} else { |
| 154 |
init(); |
| 155 |
} |
| 156 |
}()); |
| 157 |
|