Gift Wrap
A plain cardboard box gets wrapped. Patterned paper folds in from each side and down over the top, a ribbon runs round it both ways, and a bow ties itself on top: the knot pops, the loops spring out and the tails drop down.
How to use it
- Paste the snippet just before
</head>. - Add an empty element with
data-gift-wrap. Size it withfont-size: it is 9em square.
Options
Add any of these attributes to the same element.
data-color | Paper colour. Default #e11d48. |
data-pattern | dots, stripes or plain. Default dots. |
data-ribbon | Ribbon and bow colour. Default #f5c542. |
data-loop | Unwrap and wrap again every few seconds while in view. |
Nice next to a “Add gift wrap” checkbox: call animacious.giftWrap(element) when it is ticked. It fires a wrapped event when the bow is tied.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Gift Wrap: a plain cardboard box gets wrapped. Patterned paper folds in
// from each side and down over the top, a ribbon runs round it both ways,
// and a bow ties itself on top, its loops springing out and its tails
// dropping down.
// Usage: <div data-gift-wrap></div>
// Optional: data-color (paper colour, default #e11d48), data-pattern
// (dots, stripes or plain; default dots), data-ribbon (default #f5c542),
// data-loop (unwrap and wrap again every few seconds while in view).
// Size it with font-size: it is 9em wide and 9em tall.
// Fires a "wrapped" event when the bow is tied. Script use:
// animacious.giftWrap(el) wraps it again.
var DEFAULTS = { color: '#e11d48', pattern: 'dots', ribbon: '#f5c542', hold: 2200, gap: 700 };
var css =
'.gwr{position:relative;display:block;width:9em;height:9em;max-width:100%}' +
'.gwr-box{position:absolute;left:.5em;right:.5em;bottom:.3em;height:5.6em;perspective:28em}' +
'.gwr-card{position:absolute;inset:0;border-radius:.3em;background:linear-gradient(90deg,transparent 44%,rgba(255,255,255,.35) 44% 56%,transparent 56%),#c89b6d;' +
'box-shadow:inset 0 -.4em 0 rgba(0,0,0,.08),0 .5em 1em rgba(15,15,30,.15)}' +
'.gwr-card::before{content:"";position:absolute;left:0;right:0;top:0;height:1.1em;border-radius:.3em .3em 0 0;background:rgba(255,255,255,.18);border-bottom:1px solid rgba(0,0,0,.1)}' +
'.gwr-flap{position:absolute;top:0;bottom:0;backface-visibility:hidden;background:var(--gwr-p);background-color:var(--gwr-c)}' +
'.gwr-left{left:0;width:52%;border-radius:.3em 0 0 .3em;transform-origin:0 50%}' +
'.gwr-right{right:0;width:52%;border-radius:0 .3em .3em 0;transform-origin:100% 50%;box-shadow:-.15em 0 .25em rgba(0,0,0,.18)}' +
'.gwr-top{left:0;right:0;top:0;bottom:auto;height:1.4em;border-radius:.3em .3em 0 0;transform-origin:50% 0;box-shadow:0 .15em .25em rgba(0,0,0,.18)}' +
'.gwr-shade{position:absolute;inset:0;border-radius:.3em;pointer-events:none;background:linear-gradient(90deg,rgba(0,0,0,.08),transparent 20% 80%,rgba(0,0,0,.14));opacity:0}' +
'.gwr-rv,.gwr-rh{position:absolute;background:var(--gwr-r);box-shadow:0 0 0 1px rgba(0,0,0,.06)}' +
'.gwr-rv{left:50%;top:0;bottom:0;width:1em;margin-left:-.5em;transform-origin:50% 0}' +
'.gwr-rh{left:0;right:0;top:55%;height:1em;margin-top:-.5em;transform-origin:0 50%}' +
'.gwr-bow{position:absolute;left:50%;bottom:4.95em;width:5em;margin-left:-2.5em}' +
'.gwr-bow svg{display:block;width:100%;overflow:visible}' +
'.gwr-bow .gwr-part{transform-box:fill-box}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var PATTERNS = {
dots: 'radial-gradient(circle,rgba(255,255,255,.6) 0 .16em,transparent .18em) 0 0/.9em .9em,radial-gradient(circle,rgba(255,255,255,.35) 0 .1em,transparent .12em) .45em .45em/.9em .9em',
stripes: 'repeating-linear-gradient(45deg,rgba(255,255,255,.4) 0 .3em,transparent .3em .8em)',
plain: 'linear-gradient(transparent,transparent)'
};
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function bowSvg() {
// Loops pivot on the knot; tails hang from it.
return '<svg viewBox="0 0 100 60" aria-hidden="true">' +
'<path class="gwr-part gwr-tl" d="M47 40L34 58l6-1 3 3 6-19z" fill="var(--gwr-r)" style="transform-origin:100% 0"/>' +
'<path class="gwr-part gwr-tr" d="M53 40l13 18-6-1-3 3-6-19z" fill="var(--gwr-r)" style="transform-origin:0 0"/>' +
'<g class="gwr-part gwr-ll" style="transform-origin:100% 80%"><path d="M48 40C34 18 8 14 8 30c0 12 24 14 40 10z" fill="var(--gwr-r)"/>' +
'<path d="M46 38C36 26 20 22 16 30" fill="none" stroke="rgba(0,0,0,.18)" stroke-width="3" stroke-linecap="round"/></g>' +
'<g class="gwr-part gwr-lr" style="transform-origin:0 80%"><path d="M52 40C66 18 92 14 92 30c0 12-24 14-40 10z" fill="var(--gwr-r)"/>' +
'<path d="M54 38C64 26 80 22 84 30" fill="none" stroke="rgba(0,0,0,.18)" stroke-width="3" stroke-linecap="round"/></g>' +
'<rect class="gwr-part gwr-knot" x="42" y="32" width="16" height="14" rx="5" fill="var(--gwr-r)" stroke="rgba(0,0,0,.15)" stroke-width="2" style="transform-origin:50% 50%"/>' +
'</svg>';
}
function setup(el) {
var d = el.dataset;
el.classList.add('gwr');
el.style.setProperty('--gwr-c', d.color || DEFAULTS.color);
el.style.setProperty('--gwr-r', d.ribbon || DEFAULTS.ribbon);
el.style.setProperty('--gwr-p', PATTERNS[d.pattern] || PATTERNS[DEFAULTS.pattern]);
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'A present being wrapped');
el.setAttribute('role', 'img');
el.innerHTML = '<div class="gwr-box"><span class="gwr-card"></span>' +
'<span class="gwr-flap gwr-left"></span><span class="gwr-flap gwr-right"></span><span class="gwr-flap gwr-top"></span>' +
'<span class="gwr-shade"></span><span class="gwr-rv"></span><span class="gwr-rh"></span></div>' +
'<div class="gwr-bow">' + bowSvg() + '</div>';
var q = function (c) { return el.querySelector(c); };
var s = el._gwr = {
left: q('.gwr-left'), right: q('.gwr-right'), top: q('.gwr-top'), shade: q('.gwr-shade'), rv: q('.gwr-rv'), rh: q('.gwr-rh'),
ll: q('.gwr-ll'), lr: q('.gwr-lr'), knot: q('.gwr-knot'), tl: q('.gwr-tl'), tr: q('.gwr-tr'),
busy: false, visible: false, wrapped: false
};
if (reduced()) { s.wrapped = true; return; }
unwrapped(s);
new IntersectionObserver(function (entries) {
s.visible = entries[0].isIntersecting;
if (s.visible && !s.busy && (!s.played || s.waiting)) { s.waiting = false; setTimeout(function () { wrap(el); }, 300); }
}, { threshold: 0.3 }).observe(el);
}
function unwrapped(s) {
anime.utils.set(s.left, { rotateY: 100 });
anime.utils.set(s.right, { rotateY: -100 });
anime.utils.set(s.top, { rotateX: -100 });
anime.utils.set(s.shade, { opacity: 0 });
anime.utils.set(s.rv, { scaleY: 0 });
anime.utils.set(s.rh, { scaleX: 0 });
anime.utils.set([s.ll, s.lr, s.knot], { scale: 0 });
anime.utils.set([s.tl, s.tr], { scaleY: 0 });
}
function wrap(el) {
var s = el._gwr, d = el.dataset;
if (s.busy || s.wrapped) return;
s.busy = true;
s.played = true;
var tl = anime.createTimeline({
onComplete: function () {
s.busy = false;
s.wrapped = true;
el.dispatchEvent(new CustomEvent('wrapped', { bubbles: true }));
if (on(d.loop)) setTimeout(function () { unwrap(el); }, DEFAULTS.hold);
}
});
// Paper folds in from each side, then down over the top.
tl.add(s.left, { rotateY: [100, 0], duration: 650, ease: 'out(3)' }, 0)
.add(s.right, { rotateY: [-100, 0], duration: 650, ease: 'out(3)' }, 380)
.add(s.top, { rotateX: [-100, 0], duration: 550, ease: 'out(3)' }, 820)
.add(s.shade, { opacity: [0, 1], duration: 400 }, 900)
// Ribbon runs round both ways.
.add(s.rv, { scaleY: [0, 1], duration: 420, ease: 'out(3)' }, 1350)
.add(s.rh, { scaleX: [0, 1], duration: 420, ease: 'out(3)' }, 1620)
// The bow ties itself: knot, loops spring out, tails drop.
.add(s.knot, { scale: [0, 1.25, 1], duration: 380, ease: 'out(2)' }, 1980)
.add(s.ll, { scale: [0, 1], rotate: [50, 0], duration: 650, ease: 'outElastic(1, .55)' }, 2120)
.add(s.lr, { scale: [0, 1], rotate: [-50, 0], duration: 650, ease: 'outElastic(1, .55)' }, 2200)
.add(s.tl, { scaleY: [0, 1], rotate: [20, 0], duration: 500, ease: 'outBack(2)' }, 2300)
.add(s.tr, { scaleY: [0, 1], rotate: [-20, 0], duration: 500, ease: 'outBack(2)' }, 2340)
.add(s.knot, { scale: [1, 0.85, 1], duration: 260, ease: 'inOut(2)' }, 2700);
}
function unwrap(el) {
var s = el._gwr;
if (!s.visible) { s.waiting = true; s.wrapped = false; unwrapped(s); return; }
s.busy = true;
var tl = anime.createTimeline({
onComplete: function () {
s.busy = false;
s.wrapped = false;
setTimeout(function () { if (s.visible) wrap(el); else s.waiting = true; }, DEFAULTS.gap);
}
});
tl.add([s.ll, s.lr, s.knot, s.tl, s.tr], { scale: 0, duration: 300, ease: 'in(2)' }, 0)
.add([s.rv], { scaleY: 0, duration: 300, ease: 'in(2)' }, 200)
.add([s.rh], { scaleX: 0, duration: 300, ease: 'in(2)' }, 200)
.add(s.shade, { opacity: 0, duration: 300 }, 400)
.add(s.top, { rotateX: -100, duration: 400, ease: 'in(2)' }, 450)
.add(s.right, { rotateY: -100, duration: 450, ease: 'in(2)' }, 650)
.add(s.left, { rotateY: 100, duration: 450, ease: 'in(2)' }, 800)
.call(function () { anime.utils.set([s.tl, s.tr], { scale: 1, scaleY: 0 }); }, 1260);
}
window.animacious = window.animacious || {};
window.animacious.giftWrap = function (el) {
var s = el && el._gwr;
if (!s || s.busy) return;
s.wrapped = false;
unwrapped(s);
wrap(el);
};
function init() {
document.querySelectorAll('[data-gift-wrap]').forEach(function (el) {
if (el._gwr) return;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-gift-wrap style="font-size: 20px"></div>
<!-- teal paper with stripes: -->
<div data-gift-wrap data-color="#0d9488" data-pattern="stripes"></div>