| 1 |
(function () { |
| 2 |
var SVG_NS = 'http://www.w3.org/2000/svg'; |
| 3 |
|
| 4 |
var _typoKeys = { |
| 5 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 6 |
transform:'text-transform', decoration:'text-decoration', |
| 7 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 8 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 9 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 10 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 11 |
}; |
| 12 |
var _typoUnits = { size:'sizeUnit', lineHeight:'lineHeightUnit', letterSpacing:'letterSpacingUnit', wordSpacing:'wordSpacingUnit' }; |
| 13 |
var _typoUnitDefaults = { size:'px', lineHeight:'', letterSpacing:'px', wordSpacing:'px' }; |
| 14 |
function typoCssVarsForEl(el, obj, prefix) { |
| 15 |
if (!obj || typeof obj !== 'object') return; |
| 16 |
Object.keys(_typoKeys).forEach(function (k) { |
| 17 |
var v = obj[k]; if (v === undefined || v === '') return; |
| 18 |
var prop = _typoKeys[k]; |
| 19 |
var base = k.replace(/Desktop|Tablet|Mobile/, ''); |
| 20 |
var uKey = _typoUnits[base]; |
| 21 |
if (uKey && typeof v === 'number') v = v + (obj[uKey] || _typoUnitDefaults[base] || ''); |
| 22 |
el.style.setProperty('--' + prefix + prop, v); |
| 23 |
}); |
| 24 |
} |
| 25 |
|
| 26 |
function svgEl(tag, attrs) { |
| 27 |
var el = document.createElementNS(SVG_NS, tag); |
| 28 |
Object.keys(attrs).forEach(function(k) { el.setAttribute(k, attrs[k]); }); |
| 29 |
return el; |
| 30 |
} |
| 31 |
|
| 32 |
function easeFn(name) { |
| 33 |
return { 'ease-out':'cubic-bezier(0,0,0.25,1)', 'ease-in':'cubic-bezier(0.75,0,1,1)', 'linear':'linear' }[name] || 'cubic-bezier(0,0,0.25,1)'; |
| 34 |
} |
| 35 |
|
| 36 |
function initSkillRings(appEl) { |
| 37 |
var opts; |
| 38 |
try { opts = JSON.parse(appEl.getAttribute('data-opts') || '{}'); } catch(e){ opts={}; } |
| 39 |
|
| 40 |
var a = { |
| 41 |
skills: opts.skills || [], |
| 42 |
layout: opts.layout || 'row', |
| 43 |
ringSize: opts.ringSize || 140, |
| 44 |
ringGap: opts.ringGap || 14, |
| 45 |
strokeWidth: opts.strokeWidth || 10, |
| 46 |
trackColor: opts.trackColor || '#1f2937', |
| 47 |
animDuration: opts.animDuration || 1400, |
| 48 |
animEasing: opts.animEasing || 'ease-out', |
| 49 |
animDelay: opts.animDelay || 120, |
| 50 |
showPercentage: opts.showPercentage !== false, |
| 51 |
showLabel: opts.showLabel !== false, |
| 52 |
labelPosition: opts.labelPosition || 'below', |
| 53 |
lineCap: opts.lineCap || 'round', |
| 54 |
bgColor: opts.bgColor || '', |
| 55 |
labelColor: opts.labelColor || '#ffffff', |
| 56 |
pctColor: opts.pctColor || '#ffffff', |
| 57 |
pctSize: opts.pctSize || 28, |
| 58 |
labelSize: opts.labelSize || 14, |
| 59 |
paddingTop: opts.paddingTop || 60, |
| 60 |
paddingBottom: opts.paddingBottom || 60, |
| 61 |
centerLabel: opts.centerLabel || '', |
| 62 |
centerLabelColor:opts.centerLabelColor||'#ffffff' |
| 63 |
}; |
| 64 |
|
| 65 |
if (!a.skills.length) { appEl.remove(); return; } |
| 66 |
|
| 67 |
var wrap = document.createElement('div'); |
| 68 |
wrap.className = 'bkbg-sr-wrap'; |
| 69 |
var cs = wrap.style; |
| 70 |
cs.setProperty('--bkbg-sr-bg', a.bgColor || 'transparent'); |
| 71 |
cs.setProperty('--bkbg-sr-pt', a.paddingTop + 'px'); |
| 72 |
cs.setProperty('--bkbg-sr-pb', a.paddingBottom + 'px'); |
| 73 |
cs.setProperty('--bkbg-sr-dur', a.animDuration + 'ms'); |
| 74 |
cs.setProperty('--bkbg-sr-easing', easeFn(a.animEasing)); |
| 75 |
cs.setProperty('--bkbg-sr-pct-size', a.pctSize + 'px'); |
| 76 |
cs.setProperty('--bkbg-sr-label-size', a.labelSize + 'px'); |
| 77 |
cs.setProperty('--bkbg-sr-pct-color', a.pctColor); |
| 78 |
cs.setProperty('--bkbg-sr-label-color', a.labelColor); |
| 79 |
|
| 80 |
/* Apply typography CSS vars */ |
| 81 |
typoCssVarsForEl(wrap, opts.pctTypo, 'bksr-pc-'); |
| 82 |
typoCssVarsForEl(wrap, opts.labelTypo, 'bksr-lb-'); |
| 83 |
|
| 84 |
var progressEls = []; |
| 85 |
|
| 86 |
if (a.layout === 'row') { |
| 87 |
var row = document.createElement('div'); |
| 88 |
row.className = 'bkbg-sr-row'; |
| 89 |
|
| 90 |
a.skills.forEach(function(skill, i) { |
| 91 |
var size = a.ringSize; |
| 92 |
var sw = a.strokeWidth; |
| 93 |
var r = (size - sw) / 2; |
| 94 |
var circ = 2 * Math.PI * r; |
| 95 |
var cx = size / 2, cy = size / 2; |
| 96 |
var targetOffset = circ * (1 - (skill.pct || 0) / 100); |
| 97 |
|
| 98 |
var item = document.createElement('div'); |
| 99 |
item.className = 'bkbg-sr-item'; |
| 100 |
|
| 101 |
var ringWrap = document.createElement('div'); |
| 102 |
ringWrap.className = 'bkbg-sr-ring-wrap'; |
| 103 |
|
| 104 |
var svg = svgEl('svg', { width:size, height:size, viewBox:'0 0 '+size+' '+size }); |
| 105 |
|
| 106 |
/* Track */ |
| 107 |
svg.appendChild(svgEl('circle', { cx:cx, cy:cy, r:r, fill:'none', stroke:a.trackColor, 'stroke-width':sw })); |
| 108 |
|
| 109 |
/* Progress */ |
| 110 |
var prog = svgEl('circle', { |
| 111 |
cx:cx, cy:cy, r:r, fill:'none', |
| 112 |
stroke: skill.color || '#7c3aed', |
| 113 |
'stroke-width': sw, |
| 114 |
'stroke-dasharray': circ, |
| 115 |
'stroke-dashoffset': circ, /* starts at 0% */ |
| 116 |
'stroke-linecap': a.lineCap, |
| 117 |
class:'bkbg-sr-progress', |
| 118 |
transform:'rotate(-90 '+cx+' '+cy+')' |
| 119 |
}); |
| 120 |
prog.style.transitionDelay = (i * a.animDelay) + 'ms'; |
| 121 |
svg.appendChild(prog); |
| 122 |
|
| 123 |
progressEls.push({ el: prog, target: targetOffset }); |
| 124 |
|
| 125 |
/* Center label (inside) */ |
| 126 |
if (a.labelPosition === 'inside') { |
| 127 |
var center = document.createElement('div'); |
| 128 |
center.className = 'bkbg-sr-center'; |
| 129 |
if (a.showPercentage) { |
| 130 |
var pctSpan = document.createElement('span'); |
| 131 |
pctSpan.className = 'bkbg-sr-pct'; |
| 132 |
pctSpan.textContent = (skill.pct||0) + '%'; |
| 133 |
center.appendChild(pctSpan); |
| 134 |
} |
| 135 |
if (a.showLabel) { |
| 136 |
var nameSpan = document.createElement('span'); |
| 137 |
nameSpan.className = 'bkbg-sr-name-inside'; |
| 138 |
nameSpan.textContent = skill.name; |
| 139 |
center.appendChild(nameSpan); |
| 140 |
} |
| 141 |
ringWrap.appendChild(center); |
| 142 |
} |
| 143 |
|
| 144 |
ringWrap.appendChild(svg); |
| 145 |
item.appendChild(ringWrap); |
| 146 |
|
| 147 |
/* Below label */ |
| 148 |
if (a.labelPosition !== 'inside') { |
| 149 |
var below = document.createElement('div'); |
| 150 |
below.className = 'bkbg-sr-label-below'; |
| 151 |
if (a.showPercentage) { |
| 152 |
var pctD = document.createElement('div'); |
| 153 |
pctD.className = 'bkbg-sr-pct-below'; |
| 154 |
pctD.style.color = skill.color || a.pctColor; |
| 155 |
pctD.textContent = (skill.pct||0) + '%'; |
| 156 |
below.appendChild(pctD); |
| 157 |
} |
| 158 |
if (a.showLabel) { |
| 159 |
var nameD = document.createElement('div'); |
| 160 |
nameD.className = 'bkbg-sr-name-below'; |
| 161 |
nameD.textContent = skill.name; |
| 162 |
below.appendChild(nameD); |
| 163 |
} |
| 164 |
item.appendChild(below); |
| 165 |
} |
| 166 |
|
| 167 |
row.appendChild(item); |
| 168 |
}); |
| 169 |
|
| 170 |
wrap.appendChild(row); |
| 171 |
|
| 172 |
} else { |
| 173 |
/* Concentric layout */ |
| 174 |
var sw2 = a.strokeWidth; |
| 175 |
var gap = a.ringGap; |
| 176 |
var n = a.skills.length; |
| 177 |
var totalSize = a.ringSize + (n > 1 ? (n-1)*(sw2 + gap) : 0); |
| 178 |
var cx2 = totalSize/2, cy2 = totalSize/2; |
| 179 |
|
| 180 |
var conc = document.createElement('div'); |
| 181 |
conc.className = 'bkbg-sr-concentric'; |
| 182 |
|
| 183 |
var svg2 = svgEl('svg', { width:totalSize, height:totalSize, viewBox:'0 0 '+totalSize+' '+totalSize, style:'max-width:100%' }); |
| 184 |
|
| 185 |
a.skills.forEach(function(skill, i) { |
| 186 |
var r2 = (totalSize/2) - sw2/2 - i*(sw2+gap); |
| 187 |
if (r2 <= 0) return; |
| 188 |
var circ2 = 2*Math.PI*r2; |
| 189 |
var targetOffset2 = circ2*(1-(skill.pct||0)/100); |
| 190 |
|
| 191 |
svg2.appendChild(svgEl('circle',{cx:cx2,cy:cy2,r:r2,fill:'none',stroke:a.trackColor,'stroke-width':sw2})); |
| 192 |
var prog2 = svgEl('circle',{ |
| 193 |
cx:cx2,cy:cy2,r:r2,fill:'none', |
| 194 |
stroke:skill.color||'#7c3aed','stroke-width':sw2, |
| 195 |
'stroke-dasharray':circ2,'stroke-dashoffset':circ2, |
| 196 |
'stroke-linecap':a.lineCap,class:'bkbg-sr-progress', |
| 197 |
transform:'rotate(-90 '+cx2+' '+cy2+')' |
| 198 |
}); |
| 199 |
prog2.style.transitionDelay = (i*a.animDelay)+'ms'; |
| 200 |
svg2.appendChild(prog2); |
| 201 |
progressEls.push({el:prog2, target:targetOffset2}); |
| 202 |
}); |
| 203 |
|
| 204 |
/* Center label */ |
| 205 |
if (a.centerLabel) { |
| 206 |
var txt = svgEl('text',{x:cx2,y:cy2,'dominant-baseline':'middle','text-anchor':'middle',fill:a.centerLabelColor,'font-size':20,'font-weight':700}); |
| 207 |
txt.textContent = a.centerLabel; |
| 208 |
svg2.appendChild(txt); |
| 209 |
} |
| 210 |
|
| 211 |
conc.appendChild(svg2); |
| 212 |
|
| 213 |
/* Legend */ |
| 214 |
var legend = document.createElement('div'); |
| 215 |
legend.className = 'bkbg-sr-legend'; |
| 216 |
a.skills.forEach(function(skill){ |
| 217 |
var li = document.createElement('div'); li.className='bkbg-sr-legend-item'; |
| 218 |
var dot = document.createElement('div'); dot.className='bkbg-sr-legend-dot'; dot.style.background=skill.color||'#7c3aed'; |
| 219 |
var nm = document.createElement('span'); nm.className='bkbg-sr-legend-name'; nm.textContent=skill.name; |
| 220 |
var pc = document.createElement('span'); pc.className='bkbg-sr-legend-pct'; pc.style.color=skill.color||'#7c3aed'; pc.textContent=(skill.pct||0)+'%'; |
| 221 |
li.appendChild(dot); li.appendChild(nm); li.appendChild(pc); |
| 222 |
legend.appendChild(li); |
| 223 |
}); |
| 224 |
conc.appendChild(legend); |
| 225 |
wrap.appendChild(conc); |
| 226 |
} |
| 227 |
|
| 228 |
appEl.replaceWith(wrap); |
| 229 |
|
| 230 |
/* Animate when in view */ |
| 231 |
function triggerAnimation() { |
| 232 |
progressEls.forEach(function(p) { p.el.setAttribute('stroke-dashoffset', p.target); }); |
| 233 |
} |
| 234 |
|
| 235 |
if ('IntersectionObserver' in window) { |
| 236 |
var obs = new IntersectionObserver(function(entries) { |
| 237 |
if (entries[0].isIntersecting) { |
| 238 |
triggerAnimation(); |
| 239 |
obs.disconnect(); |
| 240 |
} |
| 241 |
}, { threshold: 0.2 }); |
| 242 |
obs.observe(wrap); |
| 243 |
} else { |
| 244 |
triggerAnimation(); |
| 245 |
} |
| 246 |
} |
| 247 |
|
| 248 |
document.querySelectorAll('.bkbg-sr-app').forEach(initSkillRings); |
| 249 |
})(); |
| 250 |
|