| 1 |
/* Scroll Reveal — frontend */ |
| 2 |
(function () { |
| 3 |
'use strict'; |
| 4 |
|
| 5 |
/* map animation name → { initial transform, optional extra } */ |
| 6 |
function getInitialTransform(animation, distance, scale, rotate) { |
| 7 |
var d = distance + 'px'; |
| 8 |
switch (animation) { |
| 9 |
case 'fade-up': return 'translateY(' + d + ')'; |
| 10 |
case 'fade-down': return 'translateY(-' + d + ')'; |
| 11 |
case 'fade-left': return 'translateX(' + d + ')'; |
| 12 |
case 'fade-right': return 'translateX(-' + d + ')'; |
| 13 |
case 'slide-up': return 'translateY(' + d + ')'; |
| 14 |
case 'slide-down': return 'translateY(-' + d + ')'; |
| 15 |
case 'slide-left': return 'translateX(' + d + ')'; |
| 16 |
case 'slide-right': return 'translateX(-' + d + ')'; |
| 17 |
case 'zoom-in': return 'scale(' + (scale / 100) + ')'; |
| 18 |
case 'zoom-out': return 'scale(' + (scale > 100 ? scale / 100 : 1.1) + ')'; |
| 19 |
case 'flip-left': return 'perspective(600px) rotateY(-90deg)'; |
| 20 |
case 'flip-right': return 'perspective(600px) rotateY(90deg)'; |
| 21 |
case 'rotate-in': return 'rotate(' + rotate + 'deg)'; |
| 22 |
case 'skew-up': return 'translateY(' + d + ') skewX(' + rotate + 'deg)'; |
| 23 |
case 'bounce-in': return 'scale(0.8)'; |
| 24 |
case 'fade-in': |
| 25 |
default: return 'none'; |
| 26 |
} |
| 27 |
} |
| 28 |
|
| 29 |
function getRevealTransition(animation, easing, duration) { |
| 30 |
var bounce = animation === 'bounce-in' ? 'cubic-bezier(0.34,1.56,0.64,1)' : easing; |
| 31 |
return 'opacity ' + duration + 'ms ' + bounce + ', transform ' + duration + 'ms ' + bounce; |
| 32 |
} |
| 33 |
|
| 34 |
function applyInitial(el, animation, distance, scale, rotate, opacity) { |
| 35 |
var transform = getInitialTransform(animation, distance, scale, rotate); |
| 36 |
el.style.opacity = (opacity / 100).toString(); |
| 37 |
el.style.transform = transform; |
| 38 |
el.style.willChange = 'opacity, transform'; |
| 39 |
} |
| 40 |
|
| 41 |
function applyRevealed(el, transition, delay) { |
| 42 |
el.style.transition = transition; |
| 43 |
if (delay) { el.style.transitionDelay = delay + 'ms'; } |
| 44 |
el.style.opacity = '1'; |
| 45 |
el.style.transform = 'none'; |
| 46 |
} |
| 47 |
|
| 48 |
function init() { |
| 49 |
var motionPreference = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)'); |
| 50 |
// Content stays readable when motion is unwanted or observers are unavailable. |
| 51 |
if ((motionPreference && motionPreference.matches) || typeof window.IntersectionObserver !== 'function') { return; } |
| 52 |
var animated = []; |
| 53 |
var observers = []; |
| 54 |
|
| 55 |
function revealWithoutMotion() { |
| 56 |
if (!motionPreference.matches) { return; } |
| 57 |
observers.forEach(function (observer) { observer.disconnect(); }); |
| 58 |
animated.forEach(function (element) { |
| 59 |
element.style.transition = 'none'; |
| 60 |
element.style.transitionDelay = '0ms'; |
| 61 |
element.style.opacity = '1'; |
| 62 |
element.style.transform = 'none'; |
| 63 |
element.style.willChange = ''; |
| 64 |
}); |
| 65 |
} |
| 66 |
|
| 67 |
function option(wrap, name, fallback, min, max) { |
| 68 |
var n = parseFloat(wrap.getAttribute('data-' + name)); |
| 69 |
return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : fallback; |
| 70 |
} |
| 71 |
|
| 72 |
document.querySelectorAll('.bksr-wrap[data-animation]').forEach(function (wrap) { |
| 73 |
if (wrap._bksrInit) { return; } |
| 74 |
wrap._bksrInit = true; |
| 75 |
|
| 76 |
var animation = wrap.getAttribute('data-animation') || 'fade-up'; |
| 77 |
var duration = option(wrap, 'duration', 600, 0, 10000); |
| 78 |
var delay = option(wrap, 'delay', 0, 0, 10000); |
| 79 |
var easing = wrap.getAttribute('data-easing') || 'ease'; |
| 80 |
var distance = option(wrap, 'distance', 32, 0, 1000); |
| 81 |
var scale = option(wrap, 'scale', 95, 0, 300); |
| 82 |
var rotate = option(wrap, 'rotate', 0, -360, 360); |
| 83 |
var opacity = option(wrap, 'opacity', 0, 0, 100); |
| 84 |
var threshold = option(wrap, 'threshold', 15, 0, 100) / 100; |
| 85 |
var once = wrap.getAttribute('data-once') !== '0'; |
| 86 |
var stagger = wrap.getAttribute('data-stagger') === '1'; |
| 87 |
var staggerDelay = option(wrap, 'stagger-delay', 100, 0, 10000); |
| 88 |
// A long composition cannot fit its requested intersection ratio in |
| 89 |
// the viewport. Cap it to a reachable ratio so it can still reveal. |
| 90 |
threshold = Math.min(threshold, window.innerHeight / Math.max(1, wrap.getBoundingClientRect().height) * 0.9); |
| 91 |
|
| 92 |
var transition = getRevealTransition(animation, easing, duration); |
| 93 |
|
| 94 |
if (stagger) { |
| 95 |
/* animate each direct child individually */ |
| 96 |
var children = Array.prototype.slice.call(wrap.children); |
| 97 |
children.forEach(function (child) { |
| 98 |
child.classList.add('bksr-child-reveal'); |
| 99 |
animated.push(child); |
| 100 |
applyInitial(child, animation, distance, scale, rotate, opacity); |
| 101 |
}); |
| 102 |
|
| 103 |
var observer = new IntersectionObserver(function (entries) { |
| 104 |
entries.forEach(function (entry) { |
| 105 |
if (!entry.isIntersecting) { return; } |
| 106 |
var kids = Array.prototype.slice.call(entry.target.children); |
| 107 |
kids.forEach(function (child, i) { |
| 108 |
applyRevealed(child, transition, delay + i * staggerDelay); |
| 109 |
}); |
| 110 |
if (once) { observer.unobserve(entry.target); } |
| 111 |
}); |
| 112 |
}, { threshold: threshold }); |
| 113 |
|
| 114 |
observer.observe(wrap); |
| 115 |
observers.push(observer); |
| 116 |
|
| 117 |
} else { |
| 118 |
/* animate whole wrapper */ |
| 119 |
applyInitial(wrap, animation, distance, scale, rotate, opacity); |
| 120 |
animated.push(wrap); |
| 121 |
|
| 122 |
var obs = new IntersectionObserver(function (entries) { |
| 123 |
entries.forEach(function (entry) { |
| 124 |
if (!entry.isIntersecting) { |
| 125 |
if (!once) { |
| 126 |
/* reset */ |
| 127 |
applyInitial(entry.target, animation, distance, scale, rotate, opacity); |
| 128 |
entry.target.style.transition = 'none'; |
| 129 |
} |
| 130 |
return; |
| 131 |
} |
| 132 |
applyRevealed(entry.target, transition, delay); |
| 133 |
if (once) { obs.unobserve(entry.target); } |
| 134 |
}); |
| 135 |
}, { threshold: threshold }); |
| 136 |
|
| 137 |
obs.observe(wrap); |
| 138 |
observers.push(obs); |
| 139 |
} |
| 140 |
}); |
| 141 |
if (motionPreference && animated.length) { |
| 142 |
if (motionPreference.addEventListener) { motionPreference.addEventListener('change', revealWithoutMotion); } |
| 143 |
else if (motionPreference.addListener) { motionPreference.addListener(revealWithoutMotion); } |
| 144 |
} |
| 145 |
} |
| 146 |
|
| 147 |
if (document.readyState === 'loading') { |
| 148 |
document.addEventListener('DOMContentLoaded', init); |
| 149 |
} else { |
| 150 |
init(); |
| 151 |
} |
| 152 |
}()); |
| 153 |
|