Peekaboo
The element creeps up from an edge, stops halfway as if checking the coast is clear, pops up, looks left and right, then ducks back out of sight.
Anyone there?
How to use it
- Paste the snippet just before
</head>. - Put the element inside a box with
overflow: hidden, against the edge it should peek from. Where you place it is where it peeks up to. - Add
data-peekabooto it. It peeks once when the box scrolls into view; adddata-loopto keep peeking.
Options
Add any of these attributes to the same element.
data-edge | The edge it hides behind: bottom, top, left or right. Default bottom. |
data-angle | How far it tilts as it looks around, in degrees. Default 12. |
data-duration | How long one peek takes. Default 2600 ms. |
data-delay | Wait before the first peek. Default 0 ms. |
data-loop | Peek again every so often while it’s on screen. |
data-every | How long it stays hidden between peeks, when looping. Default 1400 ms. |
data-hover | Also peek when the pointer moves over the box or it’s tapped. |
While it looks around, the element gets a CSS variable --pkb-look from -1 (left) to 1 (right), so its eyes can follow: .eyes { transform: translateX(calc(var(--pkb-look, 0) * 3px)) }. Call animacious.peekaboo(el) to make it peek from your own code.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Peekaboo: the element peeks up from an edge, looks left and right, then
// ducks back out of sight. Put it inside a parent that clips its overflow,
// sitting against the edge it should peek from; where you place it is where
// it peeks up to.
// Usage: <div style="overflow:hidden"> … <span data-peekaboo>…</span></div>
// Optional: data-edge ("bottom", default, "top", "left" or "right"),
// data-angle (degrees it tilts as it looks around, default 12),
// data-duration (ms for one peek, default 2600), data-delay (ms before the
// first peek), data-loop (peek again every so often while in view),
// data-every (ms out of sight between peeks, default 1400), data-hover
// (also peek on hover or tap of its parent).
// While it looks around the element gets a CSS variable --pkb-look, from -1
// (looking left) to 1 (looking right), so its eyes can follow:
// .eyes { transform: translateX(calc(var(--pkb-look, 0) * 3px)); }
// Script use: animacious.peekaboo(el) makes it peek from code.
var DEFAULTS = { edge: 'bottom', angle: 12, duration: 2600, delay: 0, every: 1400 };
function on(v) { return v !== undefined && v !== 'false'; }
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// How far the element has to move to be fully out of sight past its edge.
function hidden(el) {
var e = el.dataset.edge || DEFAULTS.edge;
var r = el.getBoundingClientRect();
var p = el.parentElement.getBoundingClientRect();
var t = el._pkb.t;
if (e === 'top') return -(r.bottom - t - p.top) - 4;
if (e === 'left') return -(r.right - t - p.left) - 4;
if (e === 'right') return p.right - (r.left - t) + 4;
return p.bottom - (r.top - t) + 4;
}
function draw(el, p) {
var e = el.dataset.edge || DEFAULTS.edge;
var a = el.dataset.angle === undefined ? DEFAULTS.angle : +el.dataset.angle;
var horiz = e === 'left' || e === 'right';
el._pkb.t = p.t;
el.style.transform = (horiz ? 'translateX(' : 'translateY(') + p.t.toFixed(2) + 'px) rotate(' + (p.look * a).toFixed(2) + 'deg)';
el.style.setProperty('--pkb-look', p.look.toFixed(3));
}
function setup(el) {
var e = el.dataset.edge || DEFAULTS.edge;
el._pkb = { t: 0 };
el.style.transformOrigin = { bottom: '50% 100%', top: '50% 0', left: '0 50%', right: '100% 50%' }[e] || '50% 100%';
if (getComputedStyle(el).display === 'inline') el.style.display = 'inline-block';
if (!reduced()) draw(el, { t: hidden(el), look: 0 });
}
function play(el, done) {
var s = el._pkb;
if (s.busy) { if (done) s.after = done; return; }
if (reduced()) { if (done) done(); return; }
s.busy = true;
var d = el.dataset;
var out = hidden(el);
var p = { t: out, look: 0 };
function finish() {
draw(el, p);
s.busy = false;
var after = s.after;
s.after = null;
if (done) done();
if (after) after();
}
var u = (+d.duration || DEFAULTS.duration) / 26;
anime.createTimeline({ onUpdate: function () { draw(el, p); }, onComplete: finish })
// A cautious half peek, a pause, then all the way up.
.add(p, { t: out * 0.55, duration: u * 3, ease: 'out(3)' })
.add(p, { t: out * 0.5, duration: u * 2, ease: 'inOut(2)' })
.add(p, { t: 0, duration: u * 3, ease: 'outBack(1.6)' })
// Look one way, then the other, then straight ahead.
.add(p, { look: -1, duration: u * 3, ease: 'inOut(3)' }, '+=' + u * 1.5)
.add(p, { look: 1, duration: u * 4, ease: 'inOut(3)' }, '+=' + u * 2)
.add(p, { look: 0, duration: u * 2.5, ease: 'inOut(3)' }, '+=' + u * 2)
// And duck back down, quick.
.add(p, { t: out, duration: u * 2.5, ease: 'in(3)' }, '+=' + u * 0.5);
}
function cycle(el) {
var s = el._pkb;
play(el, function () {
if (!on(el.dataset.loop)) return;
clearTimeout(s.timer);
s.timer = setTimeout(function () {
if (s.visible) cycle(el);
else s.waiting = true;
}, +el.dataset.every || DEFAULTS.every);
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target._pkbEl;
var s = el._pkb;
s.visible = e.isIntersecting;
if (!e.isIntersecting) return;
if (!s.started) {
s.started = true;
setTimeout(function () { cycle(el); }, +el.dataset.delay || DEFAULTS.delay);
} else if (s.waiting) {
s.waiting = false;
cycle(el);
}
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-peekaboo]').forEach(function (el) {
if (el._pkb || !el.parentElement) return;
setup(el);
// Watch the parent: the element itself starts out of sight.
var box = el.parentElement;
box._pkbEl = el;
if (on(el.dataset.hover)) {
box.addEventListener('pointerenter', function () { play(el); });
box.addEventListener('pointerdown', function () { play(el); });
}
if (!reduced()) io.observe(box);
});
window.addEventListener('resize', function () {
document.querySelectorAll('[data-peekaboo]').forEach(function (el) {
if (el._pkb && !el._pkb.busy && !reduced()) draw(el, { t: hidden(el), look: 0 });
});
});
}
window.animacious = window.animacious || {};
window.animacious.peekaboo = function (el) { if (el._pkb) play(el); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div class="panel" style="position: relative; overflow: hidden">
<img class="critter" src="critter.svg" alt="" data-peekaboo data-loop
style="position: absolute; bottom: 0; left: 40%">
</div>