Torn Paper Text
Hover over the text and it rips in two along a jagged line, catching for a moment before it gives way, the halves swinging apart to show a rough white edge. Move away and it mends, ready to tear somewhere new.
Torn apart
How to use it
- Paste the snippet just before
</head>. - Add
data-torn-paper-textto a heading. Adddata-paperwith a colour to put it on a paper label that tears with it.
Options
Add any of these attributes to the same element.
data-paper | Colour of a paper label behind the text. Default none: just the letters tear. |
data-angle | How far the halves swing apart, in degrees. Default 4. |
data-autoplay | Tear and mend by itself every few seconds while nobody is hovering. Handy for demos and touch screens. |
data-interval | Time between autoplay tears. Default 3200 ms. |
On a touch screen, tap to tear it and tap again to mend it. The line is different every time.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Torn Paper Text: hover over the text and it rips in two along a jagged
// line, the halves swinging apart with a rough white edge showing. Move
// away and it mends. Tap it on a touch screen.
// Usage: <h1 data-torn-paper-text data-paper="#fffaf0">Rip it up</h1>
// Optional: data-paper (colour of a paper label behind the text, which
// tears with it; default none, just the letters tear), data-angle (how far
// the halves swing apart, in degrees; default 4), data-autoplay (tear and
// mend by itself while nobody is hovering), data-interval (ms between
// autoplay tears; default 3200).
var DEFAULTS = { angle: 4, interval: 3200 };
var css =
'.tpt-wrap{position:relative;display:inline-block;vertical-align:top}' +
'.tpt-wrap.tpt-paper{filter:drop-shadow(0 .03em .05em rgba(40,20,10,.28))}' +
'.tpt-half{display:block;position:relative;transform-origin:0 55%;will-change:transform}' +
'.tpt-half+.tpt-half{position:absolute;inset:0}' +
'.tpt-piece{display:block;position:relative}' +
'.tpt-paper .tpt-piece{padding:.22em .5em;background:var(--tpt-paper)}' +
'.tpt-rim{position:absolute;inset:0;background:#fff;opacity:0}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var angle = d.angle !== undefined ? +d.angle : DEFAULTS.angle;
var interval = +d.interval || DEFAULTS.interval;
var paper = d.paper;
var wrap = document.createElement('span');
wrap.className = 'tpt-wrap' + (paper ? ' tpt-paper' : '');
if (paper) wrap.style.setProperty('--tpt-paper', paper);
var halves = [], pieces = [], rims = [];
for (var k = 0; k < 2; k++) {
var half = document.createElement('span');
half.className = 'tpt-half';
var rim = document.createElement('span');
rim.className = 'tpt-rim';
rim.setAttribute('aria-hidden', 'true');
var piece = document.createElement('span');
piece.className = 'tpt-piece';
if (k === 0) { while (el.firstChild) piece.appendChild(el.firstChild); }
else { piece.innerHTML = pieces[0].innerHTML; half.setAttribute('aria-hidden', 'true'); }
if (paper) half.appendChild(rim);
half.appendChild(piece);
wrap.appendChild(half);
halves.push(half); pieces.push(piece); rims.push(rim);
}
el.appendChild(wrap);
// A jagged line from left to right, with the odd bigger tooth.
function cut() {
var n = 22, ys = [], y = anime.utils.random(44, 58, 1), drift = anime.utils.random(-10, 10, 1) / n;
for (var i = 0; i <= n; i++) {
y += drift;
var j = Math.random() < 0.18 ? anime.utils.random(-12, 12, 1) : anime.utils.random(-5, 5, 1);
ys.push(Math.max(25, Math.min(75, y + j)));
}
function edge(off, rough) {
return ys.map(function (v, i) {
var r = rough ? anime.utils.random(-1.2, 1.2, 1) : 0;
return (i * 100 / n).toFixed(1) + '% ' + (v + off + r).toFixed(1) + '%';
});
}
var top = 'polygon(0% 0%,100% 0%,' + edge(0).reverse().join(',') + ')';
var bot = 'polygon(' + edge(-0.8).join(',') + ',100% 100%,0% 100%)';
// The rims sit just past each edge, so a rough white band shows once torn.
var topRim = 'polygon(0% 0%,100% 0%,' + edge(2.6, true).reverse().join(',') + ')';
var botRim = 'polygon(' + edge(-3, true).join(',') + ',100% 100%,0% 100%)';
[[pieces[0], top], [pieces[1], bot], [rims[0], topRim], [rims[1], botRim]].forEach(function (pair) {
pair[0].style.clipPath = pair[1];
pair[0].style.webkitClipPath = pair[1];
});
}
cut();
if (reduced()) return;
var state = { p: 0 }, anim = null, torn = false;
function draw() {
var p = state.p;
halves[0].style.transform = 'translate(' + (-0.02 * p).toFixed(3) + 'em,' + (-0.06 * p).toFixed(3) + 'em) rotate(' + (-angle * 0.55 * p).toFixed(2) + 'deg)';
halves[1].style.transform = 'translate(' + (0.03 * p).toFixed(3) + 'em,' + (0.06 * p).toFixed(3) + 'em) rotate(' + (angle * 0.45 * p).toFixed(2) + 'deg)';
rims[0].style.opacity = rims[1].style.opacity = Math.min(1, p * 4).toFixed(2);
}
function tear() {
if (torn) return;
torn = true;
if (anim) anim.pause();
if (state.p < 0.02) cut();
// Rips happen in jerks: a catch, a pause, then it gives way.
anim = anime.animate(state, {
p: [{ to: 0.22, duration: 110, ease: 'out(3)' }, { to: 0.27, duration: 160, ease: 'linear' }, { to: 1, duration: 520, ease: 'outBack(1.8)' }],
onUpdate: draw
});
}
function mend() {
if (!torn) return;
torn = false;
if (anim) anim.pause();
anim = anime.animate(state, { p: 0, duration: 760, ease: 'inOut(3)', onUpdate: draw });
}
var lastReal = -1e9;
el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') { lastReal = performance.now(); tear(); } });
el.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') { lastReal = performance.now(); mend(); } });
el.addEventListener('pointerdown', function (e) {
if (e.pointerType === 'mouse') return;
lastReal = performance.now();
if (torn) mend(); else tear();
});
if (d.autoplay === undefined || d.autoplay === 'false') return;
var timer = null;
function tick() {
if (performance.now() - lastReal > interval) {
tear();
setTimeout(function () { if (performance.now() - lastReal > interval) mend(); }, interval * 0.45);
}
timer = setTimeout(tick, interval);
}
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(tick, 700);
}).observe(el);
}
function init() {
document.querySelectorAll('[data-torn-paper-text]').forEach(function (el) {
if (el._tpt) return;
el._tpt = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-torn-paper-text data-paper="#fffaf0">Torn apart</h1>
<!-- just the letters, swinging wider: -->
<h1 data-torn-paper-text data-angle="7">Torn apart</h1>