| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function ghostTransform(idx, style, rot, ox, oy) { |
| 5 |
if (style === 'fan') { |
| 6 |
var angle = (idx + 1) * (rot / 2); |
| 7 |
var flip = idx % 2 === 0 ? 1 : -1; |
| 8 |
return 'rotate(' + (flip * angle) + 'deg) translateY(' + ((idx + 1) * oy) + 'px)'; |
| 9 |
} |
| 10 |
if (style === 'spread') { |
| 11 |
return 'translateX(' + ((idx + 1) * ox * 1.5) + 'px) rotate(' + ((idx + 1) * (rot * 0.6)) + 'deg)'; |
| 12 |
} |
| 13 |
/* pile */ |
| 14 |
return 'translateX(' + ((idx + 1) * ox * 0.5) + 'px) translateY(' + ((idx + 1) * oy) + 'px)'; |
| 15 |
} |
| 16 |
|
| 17 |
function initStack(wrap) { |
| 18 |
var stackEl = wrap.querySelector('.bkbg-cardstack'); |
| 19 |
if (!stackEl) return; |
| 20 |
|
| 21 |
var cards = Array.from(wrap.querySelectorAll('.bkbg-cardstack-card')); |
| 22 |
var dots = Array.from(wrap.querySelectorAll('.bkbg-cardstack-dot')); |
| 23 |
if (cards.length < 2) return; |
| 24 |
|
| 25 |
var style = stackEl.dataset.style || 'fan'; |
| 26 |
var rot = parseFloat(stackEl.dataset.rotation) || 5; |
| 27 |
var ox = parseFloat(stackEl.dataset.offsetX) || 8; |
| 28 |
var oy = parseFloat(stackEl.dataset.offsetY) || 8; |
| 29 |
var behind = parseInt(stackEl.dataset.behind, 10) || 2; |
| 30 |
|
| 31 |
var current = 0; |
| 32 |
|
| 33 |
/* Position all cards as a stack */ |
| 34 |
function layout() { |
| 35 |
cards.forEach(function (card, i) { |
| 36 |
if (i === current) { |
| 37 |
card.style.position = 'relative'; |
| 38 |
card.style.zIndex = cards.length; |
| 39 |
card.style.transform = ''; |
| 40 |
card.style.opacity = '1'; |
| 41 |
card.style.pointerEvents = ''; |
| 42 |
card.classList.add('is-active'); |
| 43 |
} else { |
| 44 |
var offset = (i - current + cards.length) % cards.length; |
| 45 |
if (offset > behind) { |
| 46 |
card.style.opacity = '0'; |
| 47 |
card.style.zIndex = '0'; |
| 48 |
} else { |
| 49 |
card.style.position = 'absolute'; |
| 50 |
card.style.inset = '0'; |
| 51 |
card.style.zIndex = String(cards.length - offset); |
| 52 |
card.style.transform = ghostTransform(offset - 1, style, rot, ox, oy); |
| 53 |
card.style.opacity = String(1 - offset * 0.2); |
| 54 |
card.style.pointerEvents = 'none'; |
| 55 |
} |
| 56 |
card.classList.remove('is-active'); |
| 57 |
} |
| 58 |
}); |
| 59 |
|
| 60 |
dots.forEach(function (dot, i) { |
| 61 |
if (i === current) { |
| 62 |
dot.classList.add('is-active'); |
| 63 |
dot.style.background = dot.dataset.activeColor || '#6c3fb5'; |
| 64 |
} else { |
| 65 |
dot.classList.remove('is-active'); |
| 66 |
dot.style.background = '#d1d5db'; |
| 67 |
} |
| 68 |
}); |
| 69 |
} |
| 70 |
|
| 71 |
function advance() { |
| 72 |
cards[current].classList.add('is-exit'); |
| 73 |
var prev = current; |
| 74 |
current = (current + 1) % cards.length; |
| 75 |
|
| 76 |
setTimeout(function () { |
| 77 |
cards[prev].classList.remove('is-exit'); |
| 78 |
layout(); |
| 79 |
cards[current].classList.add('is-enter'); |
| 80 |
setTimeout(function () { cards[current].classList.remove('is-enter'); }, 350); |
| 81 |
}, 240); |
| 82 |
} |
| 83 |
|
| 84 |
/* Click / touch on stack = cycle */ |
| 85 |
stackEl.addEventListener('click', advance); |
| 86 |
stackEl.addEventListener('keydown', function (e) { |
| 87 |
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(); } |
| 88 |
}); |
| 89 |
stackEl.setAttribute('tabindex', '0'); |
| 90 |
stackEl.setAttribute('role', 'button'); |
| 91 |
stackEl.setAttribute('aria-label', 'Next card'); |
| 92 |
|
| 93 |
/* Dot nav */ |
| 94 |
dots.forEach(function (dot, i) { |
| 95 |
dot.addEventListener('click', function (e) { |
| 96 |
e.stopPropagation(); |
| 97 |
if (i === current) return; |
| 98 |
current = i; |
| 99 |
layout(); |
| 100 |
}); |
| 101 |
}); |
| 102 |
|
| 103 |
layout(); |
| 104 |
|
| 105 |
/* Animate in on scroll */ |
| 106 |
if (stackEl.dataset.animate === '1') { |
| 107 |
if ('IntersectionObserver' in window) { |
| 108 |
var io = new IntersectionObserver(function (entries) { |
| 109 |
entries.forEach(function (entry) { |
| 110 |
if (entry.isIntersecting) { |
| 111 |
entry.target.classList.add('is-visible'); |
| 112 |
io.unobserve(entry.target); |
| 113 |
} |
| 114 |
}); |
| 115 |
}, { threshold: 0.15 }); |
| 116 |
io.observe(stackEl); |
| 117 |
} else { |
| 118 |
stackEl.classList.add('is-visible'); |
| 119 |
} |
| 120 |
} |
| 121 |
|
| 122 |
/* Hover: fan cards outward on hover */ |
| 123 |
if (style === 'fan') { |
| 124 |
stackEl.addEventListener('mouseenter', function () { |
| 125 |
cards.forEach(function (card, i) { |
| 126 |
if (i !== current) { |
| 127 |
var offset = (i - current + cards.length) % cards.length; |
| 128 |
if (offset <= behind) { |
| 129 |
card.style.transform = ghostTransform(offset - 1, 'spread', rot * 0.7, ox * 1.8, oy * 0.5); |
| 130 |
card.style.opacity = String(1 - offset * 0.15); |
| 131 |
} |
| 132 |
} |
| 133 |
}); |
| 134 |
}); |
| 135 |
stackEl.addEventListener('mouseleave', function () { |
| 136 |
layout(); /* reset to normal arrangement */ |
| 137 |
}); |
| 138 |
} |
| 139 |
} |
| 140 |
|
| 141 |
document.querySelectorAll('.bkbg-cardstack-outer').forEach(initStack); |
| 142 |
}()); |
| 143 |
|