| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var _typoKeys = [['family','font-family'],['weight','font-weight'],['style','font-style'],['decoration','text-decoration'],['transform','text-transform'],['sizeDesktop','font-size-d'],['sizeTablet','font-size-t'],['sizeMobile','font-size-m'],['sizeUnit','*unit'],['lineHeightDesktop','line-height-d'],['lineHeightTablet','line-height-t'],['lineHeightMobile','line-height-m'],['letterSpacingDesktop','letter-spacing-d'],['letterSpacingTablet','letter-spacing-t'],['letterSpacingMobile','letter-spacing-m'],['wordSpacingDesktop','word-spacing-d'],['wordSpacingTablet','word-spacing-t'],['wordSpacingMobile','word-spacing-m']]; |
| 5 |
function typoCssVarsForEl(el, obj, prefix) { |
| 6 |
if (!obj || typeof obj !== 'object') return; |
| 7 |
var unit = obj.sizeUnit || 'px'; |
| 8 |
for (var i = 0; i < _typoKeys.length; i++) { |
| 9 |
var k = _typoKeys[i][0], p = _typoKeys[i][1]; |
| 10 |
if (p === '*unit' || obj[k] == null || obj[k] === '') continue; |
| 11 |
var v = obj[k]; |
| 12 |
if (p === 'font-size-d' || p === 'font-size-t' || p === 'font-size-m') v = v + unit; |
| 13 |
el.style.setProperty(prefix + p, '' + v); |
| 14 |
} |
| 15 |
} |
| 16 |
|
| 17 |
// ── Unit conversion tables ────────────────────────────────────────────── |
| 18 |
var DIST_TO_M = { |
| 19 |
'm': 1, |
| 20 |
'km': 1000, |
| 21 |
'miles': 1609.344, |
| 22 |
'feet': 0.3048, |
| 23 |
'nautical miles': 1852 |
| 24 |
}; |
| 25 |
|
| 26 |
var TIME_TO_S = { |
| 27 |
'seconds': 1, |
| 28 |
'minutes': 60, |
| 29 |
'hours': 3600, |
| 30 |
'days': 86400 |
| 31 |
}; |
| 32 |
|
| 33 |
var MPS_TO_SPEED = { |
| 34 |
'm/s': 1, |
| 35 |
'km/h': 3.6, |
| 36 |
'mph': 2.23694, |
| 37 |
'knots': 1.94384 |
| 38 |
}; |
| 39 |
|
| 40 |
var SPEED_UNITS = ['km/h', 'mph', 'm/s', 'knots']; |
| 41 |
var DIST_UNITS = ['km', 'miles', 'm', 'feet', 'nautical miles']; |
| 42 |
var TIME_UNITS = ['seconds', 'minutes', 'hours', 'days']; |
| 43 |
|
| 44 |
var REAL_WORLD = [ |
| 45 |
{ label:'Walking pace', speed:'5 km/h', dist:'', time:'' }, |
| 46 |
{ label:'City cycling', speed:'20 km/h', dist:'', time:'' }, |
| 47 |
{ label:'Highway car', speed:'110 km/h', dist:'', time:'' }, |
| 48 |
{ label:'Commercial aircraft', speed:'900 km/h', dist:'', time:'' }, |
| 49 |
{ label:'Speed of sound', speed:'1235 km/h',dist:'', time:'' }, |
| 50 |
{ label:'Earth orbit speed', speed:'28000 km/h',dist:'', time:'' } |
| 51 |
]; |
| 52 |
|
| 53 |
// ── Helper: format number neatly ──────────────────────────────────────── |
| 54 |
function fmt(n) { |
| 55 |
if (!isFinite(n)) return '—'; |
| 56 |
if (Math.abs(n) >= 1000) return n.toLocaleString(undefined, { maximumFractionDigits: 2 }); |
| 57 |
if (Math.abs(n) >= 1) return parseFloat(n.toFixed(4)).toString(); |
| 58 |
return parseFloat(n.toFixed(6)).toString(); |
| 59 |
} |
| 60 |
|
| 61 |
// ── Helper: format seconds as human readable duration ────────────────── |
| 62 |
function fmtDuration(secs) { |
| 63 |
if (!isFinite(secs) || secs < 0) return '—'; |
| 64 |
var d = Math.floor(secs / 86400); |
| 65 |
var h = Math.floor((secs % 86400) / 3600); |
| 66 |
var m = Math.floor((secs % 3600) / 60); |
| 67 |
var s = secs % 60; |
| 68 |
var parts = []; |
| 69 |
if (d > 0) parts.push(d + 'd'); |
| 70 |
if (h > 0) parts.push(h + 'h'); |
| 71 |
if (m > 0) parts.push(m + 'min'); |
| 72 |
if (s > 0 || parts.length === 0) parts.push(parseFloat(s.toFixed(2)) + 's'); |
| 73 |
return parts.join(' '); |
| 74 |
} |
| 75 |
|
| 76 |
// ── Helper: create DOM element ────────────────────────────────────────── |
| 77 |
function el(tag, attrs, children) { |
| 78 |
var node = document.createElement(tag); |
| 79 |
if (attrs) { |
| 80 |
Object.keys(attrs).forEach(function (k) { |
| 81 |
if (k === 'style') { |
| 82 |
Object.assign(node.style, attrs[k]); |
| 83 |
} else if (k === 'class') { |
| 84 |
node.className = attrs[k]; |
| 85 |
} else { |
| 86 |
node.setAttribute(k, attrs[k]); |
| 87 |
} |
| 88 |
}); |
| 89 |
} |
| 90 |
if (children) { |
| 91 |
if (typeof children === 'string') { |
| 92 |
node.textContent = children; |
| 93 |
} else if (Array.isArray(children)) { |
| 94 |
children.forEach(function (c) { if (c) node.appendChild(c); }); |
| 95 |
} else { |
| 96 |
node.appendChild(children); |
| 97 |
} |
| 98 |
} |
| 99 |
return node; |
| 100 |
} |
| 101 |
|
| 102 |
function text(str) { return document.createTextNode(str); } |
| 103 |
|
| 104 |
function makeSelect(options, selected) { |
| 105 |
var sel = document.createElement('select'); |
| 106 |
sel.className = 'bkbg-spd-unit-select'; |
| 107 |
options.forEach(function (opt) { |
| 108 |
var o = document.createElement('option'); |
| 109 |
o.value = opt; o.textContent = opt; |
| 110 |
if (opt === selected) o.selected = true; |
| 111 |
sel.appendChild(o); |
| 112 |
}); |
| 113 |
return sel; |
| 114 |
} |
| 115 |
|
| 116 |
function makeInput(placeholder, value) { |
| 117 |
var inp = document.createElement('input'); |
| 118 |
inp.type = 'number'; |
| 119 |
inp.className = 'bkbg-spd-input'; |
| 120 |
inp.placeholder = placeholder || 'Enter value…'; |
| 121 |
inp.min = '0'; |
| 122 |
inp.step = 'any'; |
| 123 |
if (value !== undefined && value !== '') inp.value = value; |
| 124 |
return inp; |
| 125 |
} |
| 126 |
|
| 127 |
// ── Build block ───────────────────────────────────────────────────────── |
| 128 |
function buildBlock(root) { |
| 129 |
var o; |
| 130 |
try { o = JSON.parse(root.getAttribute('data-opts')); } catch (e) { return; } |
| 131 |
|
| 132 |
// CSS variables |
| 133 |
root.style.setProperty('--spd-accent', o.accentColor || '#3b82f6'); |
| 134 |
root.style.setProperty('--spd-speed-color', o.speedColor || '#8b5cf6'); |
| 135 |
root.style.setProperty('--spd-dist-color', o.distColor || '#0ea5e9'); |
| 136 |
root.style.setProperty('--spd-time-color', o.timeColor || '#22c55e'); |
| 137 |
root.style.setProperty('--spd-title-color', o.titleColor || '#111827'); |
| 138 |
root.style.setProperty('--spd-subtitle-color',o.subtitleColor|| '#6b7280'); |
| 139 |
root.style.setProperty('--spd-label-color', o.labelColor || '#374151'); |
| 140 |
root.style.setProperty('--spd-max-width', (o.contentMaxWidth || 680) + 'px'); |
| 141 |
root.style.setProperty('--spd-card-bg', o.cardBg || '#ffffff'); |
| 142 |
root.style.setProperty('--spd-input-bg', o.inputBg || '#f8fafc'); |
| 143 |
root.style.setProperty('--spd-result-bg', o.resultBg || '#dbeafe'); |
| 144 |
typoCssVarsForEl(root, o.titleTypo, '--bkspd-tt-'); |
| 145 |
typoCssVarsForEl(root, o.subtitleTypo, '--bkspd-st-'); |
| 146 |
|
| 147 |
// State |
| 148 |
var solveFor = o.defaultSolveFor || 'speed'; |
| 149 |
var speedUnit = o.defaultSpeedUnit || 'km/h'; |
| 150 |
var distUnit = o.defaultDistUnit || 'km'; |
| 151 |
var timeUnit = o.defaultTimeUnit || 'hours'; |
| 152 |
|
| 153 |
var FIELDS = { |
| 154 |
speed: { label:'⚡ Speed', color:o.speedColor||'#8b5cf6', units:SPEED_UNITS, getUnit:function(){ return speedUnit; }, setUnit:function(u){ speedUnit=u; } }, |
| 155 |
dist: { label:'📏 Distance', color:o.distColor ||'#0ea5e9', units:DIST_UNITS, getUnit:function(){ return distUnit; }, setUnit:function(u){ distUnit=u; } }, |
| 156 |
time: { label:'⏱️ Time', color:o.timeColor ||'#22c55e', units:TIME_UNITS, getUnit:function(){ return timeUnit; }, setUnit:function(u){ timeUnit=u; } } |
| 157 |
}; |
| 158 |
|
| 159 |
// Wrap |
| 160 |
var wrap = el('div', { class:'bkbg-spd-wrap', style:{ background: o.sectionBg || '#eff6ff' } }); |
| 161 |
root.appendChild(wrap); |
| 162 |
|
| 163 |
// Title + subtitle |
| 164 |
if (o.title) { |
| 165 |
var titleEl = el('h3', { class:'bkbg-spd-title' }, o.title); |
| 166 |
wrap.appendChild(titleEl); |
| 167 |
} |
| 168 |
if (o.subtitle) { |
| 169 |
var sub = el('p', { class:'bkbg-spd-subtitle' }, o.subtitle); |
| 170 |
wrap.appendChild(sub); |
| 171 |
} |
| 172 |
|
| 173 |
// Solve-for tabs |
| 174 |
wrap.appendChild(el('div', { class:'bkbg-spd-tab-label' }, 'Solve for…')); |
| 175 |
var tabsRow = el('div', { class:'bkbg-spd-tabs' }); |
| 176 |
wrap.appendChild(tabsRow); |
| 177 |
|
| 178 |
var tabBtns = {}; |
| 179 |
['speed','dist','time'].forEach(function (key) { |
| 180 |
var f = FIELDS[key]; |
| 181 |
var btn = el('button', { class:'bkbg-spd-tab' + (key === solveFor ? ' active' : '') }, f.label); |
| 182 |
tabBtns[key] = btn; |
| 183 |
tabsRow.appendChild(btn); |
| 184 |
}); |
| 185 |
|
| 186 |
// Input fields |
| 187 |
var inputsGrid = el('div', { class:'bkbg-spd-inputs' }); |
| 188 |
wrap.appendChild(inputsGrid); |
| 189 |
|
| 190 |
var inputEls = {}; // key → input element |
| 191 |
var selectEls = {}; // key → select element |
| 192 |
var fieldDivs = {}; // key → field wrapper div |
| 193 |
|
| 194 |
['speed','dist','time'].forEach(function (key) { |
| 195 |
var f = FIELDS[key]; |
| 196 |
var isRes = key === solveFor; |
| 197 |
|
| 198 |
var fieldDiv = el('div', { |
| 199 |
class: 'bkbg-spd-field' + (isRes ? ' is-result' : ''), |
| 200 |
style: { background: isRes ? '' : (o.cardBg||'#fff') } |
| 201 |
}); |
| 202 |
fieldDivs[key] = fieldDiv; |
| 203 |
|
| 204 |
var lbl = el('div', { class:'bkbg-spd-field-label', style:{ color: f.color } }, |
| 205 |
f.label + (isRes ? ' (Result)' : '')); |
| 206 |
fieldDiv.appendChild(lbl); |
| 207 |
|
| 208 |
var inp = makeInput('Enter value…'); |
| 209 |
inp.disabled = isRes; |
| 210 |
if (isRes) { inp.value = ''; inp.placeholder = '—'; } |
| 211 |
inputEls[key] = inp; |
| 212 |
fieldDiv.appendChild(inp); |
| 213 |
|
| 214 |
var sel = makeSelect(f.units, f.getUnit()); |
| 215 |
selectEls[key] = sel; |
| 216 |
fieldDiv.appendChild(sel); |
| 217 |
|
| 218 |
inputsGrid.appendChild(fieldDiv); |
| 219 |
}); |
| 220 |
|
| 221 |
// Calculate button |
| 222 |
var calcBtn = el('button', { class:'bkbg-spd-calc-btn' }, '⚡ Calculate'); |
| 223 |
wrap.appendChild(calcBtn); |
| 224 |
|
| 225 |
// Result zone (conversions + formula) |
| 226 |
var resultZone = el('div', {}); |
| 227 |
wrap.appendChild(resultZone); |
| 228 |
|
| 229 |
// ── Core calculation ── |
| 230 |
function solve() { |
| 231 |
resultZone.innerHTML = ''; |
| 232 |
|
| 233 |
var speedVal = parseFloat(inputEls.speed.value); |
| 234 |
var distVal = parseFloat(inputEls.dist.value); |
| 235 |
var timeVal = parseFloat(inputEls.time.value); |
| 236 |
|
| 237 |
// Convert inputs to SI base units |
| 238 |
var speedMPS = speedVal * (1 / (MPS_TO_SPEED[speedUnit] || 1)); |
| 239 |
var distM = distVal * (DIST_TO_M[distUnit] || 1); |
| 240 |
var timeS = timeVal * (TIME_TO_S[timeUnit] || 1); |
| 241 |
|
| 242 |
var resultStr = '—'; |
| 243 |
var resultUnit = ''; |
| 244 |
|
| 245 |
if (solveFor === 'speed') { |
| 246 |
var mps = distM / timeS; |
| 247 |
var out = mps * (MPS_TO_SPEED[speedUnit] || 1); |
| 248 |
inputEls.speed.value = isFinite(out) ? fmt(out) : ''; |
| 249 |
inputEls.speed.placeholder = isFinite(out) ? '' : 'Need valid inputs'; |
| 250 |
resultStr = isFinite(out) ? fmt(out) + ' ' + speedUnit : '—'; |
| 251 |
resultUnit = 'speed'; |
| 252 |
|
| 253 |
if (isFinite(mps) && o.showUnitConversions) { |
| 254 |
appendConversions(resultZone, 'Speed', o.speedColor||'#8b5cf6', SPEED_UNITS, function(u){ |
| 255 |
return fmt(mps * (MPS_TO_SPEED[u]||1)) + ' ' + u; |
| 256 |
}); |
| 257 |
} |
| 258 |
} else if (solveFor === 'dist') { |
| 259 |
var dm = speedMPS * timeS; |
| 260 |
var outD = dm / (DIST_TO_M[distUnit] || 1); |
| 261 |
inputEls.dist.value = isFinite(outD) ? fmt(outD) : ''; |
| 262 |
inputEls.dist.placeholder = isFinite(outD) ? '' : 'Need valid inputs'; |
| 263 |
resultStr = isFinite(outD) ? fmt(outD) + ' ' + distUnit : '—'; |
| 264 |
resultUnit = 'dist'; |
| 265 |
|
| 266 |
if (isFinite(dm) && o.showUnitConversions) { |
| 267 |
appendConversions(resultZone, 'Distance', o.distColor||'#0ea5e9', DIST_UNITS, function(u){ |
| 268 |
return fmt(dm / (DIST_TO_M[u]||1)) + ' ' + u; |
| 269 |
}); |
| 270 |
} |
| 271 |
} else { |
| 272 |
var ts = distM / speedMPS; |
| 273 |
var outT = ts / (TIME_TO_S[timeUnit] || 1); |
| 274 |
inputEls.time.value = isFinite(outT) ? fmt(outT) : ''; |
| 275 |
inputEls.time.placeholder = isFinite(outT) ? '' : 'Need valid inputs'; |
| 276 |
resultStr = isFinite(ts) ? fmtDuration(ts) : '—'; |
| 277 |
resultUnit = 'time'; |
| 278 |
|
| 279 |
if (isFinite(ts) && o.showUnitConversions) { |
| 280 |
appendConversions(resultZone, 'Time', o.timeColor||'#22c55e', TIME_UNITS, function(u){ |
| 281 |
return fmt(ts / (TIME_TO_S[u]||1)) + ' ' + u; |
| 282 |
}); |
| 283 |
} |
| 284 |
} |
| 285 |
|
| 286 |
if (o.showFormula) appendFormula(resultZone); |
| 287 |
if (o.showExamples) appendExamples(resultZone); |
| 288 |
} |
| 289 |
|
| 290 |
function appendConversions(zone, heading, color, units, fmtFn) { |
| 291 |
var conv = el('div', { class:'bkbg-spd-conversions', style:{ background: o.cardBg||'#fff' } }); |
| 292 |
var h = el('div', { class:'bkbg-spd-conv-heading' }, '🔁 ' + heading + ' in all units'); |
| 293 |
conv.appendChild(h); |
| 294 |
|
| 295 |
var grid = el('div', { class:'bkbg-spd-conv-grid' }); |
| 296 |
units.forEach(function (u) { |
| 297 |
var item = el('div', { class:'bkbg-spd-conv-item' }); |
| 298 |
var uLbl = el('div', { class:'bkbg-spd-conv-unit' }, u); |
| 299 |
var uVal = el('div', { class:'bkbg-spd-conv-val', style:{ color: color } }, fmtFn(u)); |
| 300 |
item.appendChild(uLbl); |
| 301 |
item.appendChild(uVal); |
| 302 |
grid.appendChild(item); |
| 303 |
}); |
| 304 |
conv.appendChild(grid); |
| 305 |
zone.appendChild(conv); |
| 306 |
} |
| 307 |
|
| 308 |
function appendFormula(zone) { |
| 309 |
var box = el('div', { class:'bkbg-spd-formula', style:{ background: o.cardBg||'#fff' } }); |
| 310 |
[ |
| 311 |
{ label:'⚡ Speed = Distance ÷ Time', color: o.speedColor||'#8b5cf6' }, |
| 312 |
{ label:'📏 Distance = Speed × Time', color: o.distColor ||'#0ea5e9' }, |
| 313 |
{ label:'⏱️ Time = Distance ÷ Speed', color: o.timeColor ||'#22c55e' } |
| 314 |
].forEach(function (f) { |
| 315 |
box.appendChild(el('span', { class:'bkbg-spd-formula-item', style:{ color: f.color } }, f.label)); |
| 316 |
}); |
| 317 |
zone.appendChild(box); |
| 318 |
} |
| 319 |
|
| 320 |
function appendExamples(zone) { |
| 321 |
var box = el('div', { class:'bkbg-spd-examples', style:{ background: o.cardBg||'#fff' } }); |
| 322 |
box.appendChild(el('div', { class:'bkbg-spd-examples-heading' }, '🌍 Real-world speeds')); |
| 323 |
REAL_WORLD.forEach(function (ex) { |
| 324 |
var row = el('div', { class:'bkbg-spd-example-row' }); |
| 325 |
row.appendChild(text(ex.label)); |
| 326 |
row.appendChild(el('strong', { style:{ color: o.accentColor||'#3b82f6' } }, ex.speed)); |
| 327 |
box.appendChild(row); |
| 328 |
}); |
| 329 |
zone.appendChild(box); |
| 330 |
} |
| 331 |
|
| 332 |
// ── Apply solve-for mode ── |
| 333 |
function applySolveFor(key) { |
| 334 |
solveFor = key; |
| 335 |
|
| 336 |
// Update tab buttons |
| 337 |
['speed','dist','time'].forEach(function (k) { |
| 338 |
tabBtns[k].className = 'bkbg-spd-tab' + (k === key ? ' active' : ''); |
| 339 |
}); |
| 340 |
|
| 341 |
// Update field classes + input disabled state + label |
| 342 |
var f = FIELDS[key]; |
| 343 |
['speed','dist','time'].forEach(function (k) { |
| 344 |
var isRes = k === key; |
| 345 |
var fd = fieldDivs[k]; |
| 346 |
fd.className = 'bkbg-spd-field' + (isRes ? ' is-result' : ''); |
| 347 |
fd.style.background = isRes ? '' : (o.cardBg||'#fff'); |
| 348 |
inputEls[k].disabled = isRes; |
| 349 |
if (isRes) { inputEls[k].value = ''; inputEls[k].placeholder = '—'; } |
| 350 |
// Update label text |
| 351 |
fd.querySelector('.bkbg-spd-field-label').textContent = FIELDS[k].label + (isRes ? ' (Result)' : ''); |
| 352 |
}); |
| 353 |
|
| 354 |
resultZone.innerHTML = ''; |
| 355 |
if (o.showFormula) appendFormula(resultZone); |
| 356 |
if (o.showExamples) appendExamples(resultZone); |
| 357 |
} |
| 358 |
|
| 359 |
// ── Wire events ── |
| 360 |
['speed','dist','time'].forEach(function (key) { |
| 361 |
tabBtns[key].addEventListener('click', function () { applySolveFor(key); }); |
| 362 |
|
| 363 |
selectEls[key].addEventListener('change', function () { |
| 364 |
FIELDS[key].setUnit(selectEls[key].value); |
| 365 |
if (key === 'speed') speedUnit = selectEls[key].value; |
| 366 |
if (key === 'dist') distUnit = selectEls[key].value; |
| 367 |
if (key === 'time') timeUnit = selectEls[key].value; |
| 368 |
}); |
| 369 |
|
| 370 |
inputEls[key].addEventListener('input', function () { |
| 371 |
// auto-calculate only when 2 non-result inputs are filled |
| 372 |
var keys = ['speed','dist','time'].filter(function(k){ return k !== solveFor; }); |
| 373 |
var bothFilled = keys.every(function(k){ return inputEls[k].value.trim() !== '' && !isNaN(parseFloat(inputEls[k].value)); }); |
| 374 |
if (bothFilled) solve(); |
| 375 |
}); |
| 376 |
}); |
| 377 |
|
| 378 |
calcBtn.addEventListener('click', solve); |
| 379 |
|
| 380 |
// ── Initial state ── |
| 381 |
if (o.showFormula) appendFormula(resultZone); |
| 382 |
if (o.showExamples) appendExamples(resultZone); |
| 383 |
} |
| 384 |
|
| 385 |
// ── Init all blocks on page ───────────────────────────────────────────── |
| 386 |
document.querySelectorAll('.bkbg-spd-app').forEach(buildBlock); |
| 387 |
|
| 388 |
})(); |
| 389 |
|