003 / Sequence
dot → arrow → check
// Mechanisme: content-svg-morph-minimal (techniek #33 — SVG morphing) // Drie SVG-paden (dot -> arrow -> check) waarvan er één zichtbaar is. // GSAP attr-tween op het 'd'-attribuut van het visible-path naar het volgende path. // IntersectionObserver triggert auto-loop wanneer in viewport, pauzeert bij uitscroll. // Subtiel: 1.5s morph duration, 2s gap tussen morphs, fade-cross tussen paths via opacity. import gsap from 'https://esm.sh/[email protected]'; const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const root = document.querySelector('.svg-morph-minimal'); if (root) { const paths = root.querySelectorAll('.morph-path'); if (reduce) { paths.forEach((p, i) => p.setAttribute('opacity', i === paths.length - 1 ? '1' : '0')); } else if (paths.length >= 2) { let idx = 0; const active = paths[0]; active.setAttribute('opacity', '1'); paths.forEach((p, i) => { if (i !== 0) p.setAttribute('opacity', '0'); }); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting && !root.dataset.running) { root.dataset.running = '1'; const tick = () => { if (!root.dataset.running) return; const next = (idx + 1) % paths.length; const target = paths[next].getAttribute('d'); if (target) gsap.to(active, { attr: { d: target }, duration: 1.5, ease: 'power2.inOut' }); idx = next; setTimeout(tick, 3500); }; tick(); } else if (!e.isIntersecting) { delete root.dataset.running; } }); }, { threshold: 0.4 }); io.observe(root); } }
<!-- Skeleton: content-svg-morph-minimal -->
<section class="content-morph-min">
<p class="eyebrow">003 / Sequence</p>
<h2 class="title">Een gebaar.<br/>Drie betekenissen.</h2>
<div class="svg-morph-minimal" aria-hidden="true">
<svg viewBox="0 0 200 200" width="160" height="160">
<path class="morph-path" d="M100,90 C106,90 110,94 110,100 C110,106 106,110 100,110 C94,110 90,106 90,100 C90,94 94,90 100,90Z"/>
<path class="morph-path" d="M40,100 L160,100 L130,70 M160,100 L130,130"/>
<path class="morph-path" d="M50,105 L85,140 L160,65"/>
</svg>
</div>
<p class="caption">dot → arrow → check</p>
</section> /* Styling: content-svg-morph-minimal — cream + ink, Inter, generous whitespace */
:root {
--block-bg: #F4F1EB;
--block-fg: #0A0A0A;
--block-accent: rgba(10,10,10,0.4);
}
.content-morph-min {
background: var(--block-bg);
color: var(--block-fg);
font-family: 'Inter', system-ui, sans-serif;
padding: clamp(6rem, 14vw, 14rem) clamp(2rem, 8vw, 10rem);
display: grid;
gap: clamp(3rem, 6vw, 5rem);
justify-items: start;
min-height: 80vh;
}
.eyebrow {
font-size: 0.75rem; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--block-accent); margin: 0;
}
.title {
font-size: clamp(2rem, 4.5vw, 4rem); font-weight: 300; line-height: 1.05;
letter-spacing: -0.02em; margin: 0; max-width: 18ch;
}
.svg-morph-minimal svg path {
fill: none; stroke: var(--block-fg); stroke-width: 1.5;
stroke-linecap: round; stroke-linejoin: round;
}
.caption {
font-size: 0.8rem; letter-spacing: 0.04em;
color: var(--block-accent); margin: 0; font-feature-settings: 'tnum';
}