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/gsap@3.12.5';
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';
}