Magazine page turn
A two-page magazine spread whose leaves turn one by one, with a shadow that follows the curl.
The bottom corner of a page curls up and folds back across the sheet, uncovering the page beneath.
The fold is a diagonal line x + y = c that moves as you scroll. The script clips the page to the part on one side of the line, then mirrors the cut-off part across the same line to draw the folded-back flap. Both shapes are plain clip-path polygons recalculated in pixels on every frame.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Corner page peel</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }
body { background: #3a3f58; color: #22223b; }
.track { height: 320vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; }
.sheet { position: relative; width: min(80vw, 520px); aspect-ratio: .75; filter: drop-shadow(0 20px 30px rgba(0,0,0,.4)); }
.page { position: absolute; inset: 0; padding: 10%; border-radius: 4px; font-family: Georgia, serif; }
.page h2 { font-size: clamp(2rem, 6vw, 3rem); margin: 0 0 .4em; }
.page p { font-size: clamp(1rem, 2.4vw, 1.25rem); line-height: 1.5; }
.under { background: radial-gradient(circle at 70% 75%, #f2e9e4 0 18%, transparent 19%), #c9ada7; }
.top { background: #fdfaf5; }
.hint { position: absolute; right: 8%; bottom: 6%; opacity: .5; font-family: system-ui, sans-serif; }
.flap { position: absolute; inset: 0; background: linear-gradient(135deg, #e9e2d6, #fff 45%, #d9d1c3); pointer-events: none; }
</style>
</head>
<body>
<section class="track">
<div class="stage">
<div class="sheet" id="sheet">
<div class="page under"><h2>Page two</h2><p>Underneath, something new was waiting the whole time.</p></div>
<div class="page top" id="top"><h2>Page one</h2><p>Scroll to peel this page from its corner. The fold follows your scroll in both directions.</p><p class="hint">↘ grab here</p></div>
<div class="flap" id="flap"></div>
</div>
</div>
</section>
<script>
const sheet = document.getElementById('sheet');
const pageTop = document.getElementById('top');
const flap = document.getElementById('flap');
const track = document.querySelector('.track');
/* keep the part of polygon pts where x + y <= c (or >= c when keepBelow is false) */
function clip(pts, c, keepBelow) {
const inside = p => keepBelow ? p[0] + p[1] <= c : p[0] + p[1] >= c;
const out = [];
for (let i = 0; i < pts.length; i++) {
const a = pts[i], b = pts[(i + 1) % pts.length];
const ia = inside(a), ib = inside(b);
if (ia) out.push(a);
if (ia !== ib) {
const t = (c - a[0] - a[1]) / ((b[0] + b[1]) - (a[0] + a[1]));
out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
}
}
return out;
}
const poly = pts => pts.length ? 'polygon(' + pts.map(p => p[0].toFixed(1) + 'px ' + p[1].toFixed(1) + 'px').join(',') + ')' : 'polygon(0 0,0 0,0 0)';
function update() {
const r = track.getBoundingClientRect();
const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
const W = sheet.offsetWidth, H = sheet.offsetHeight;
const rect = [[0, 0], [W, 0], [W, H], [0, H]];
const c = W + H - p * (W + H) * 1.02;
pageTop.style.clipPath = poly(clip(rect, c, true));
const cut = clip(rect, c, false).map(([x, y]) => [c - y, c - x]);
flap.style.clipPath = poly(cut);
flap.style.opacity = cut.length ? 1 : 0;
}
addEventListener('scroll', update, { passive: true });
addEventListener('resize', update);
update();
</script>
</body>
</html>