Workshop kickoff
Stakeholder mapping, research, scope.
Stakeholder mapping, research, scope.
Gap-analyse op huidige performance.
Archetype, claim, doelgroep-fit.
Astro, CMS, integraties, tests.
DNS, deliverability, GA4.
A/B, content, paid scaling.
// Mechanisme: Matter.js physics op ECHTE DOM-cards (laag A) // Geen <canvas>-render — Matter berekent alleen body-posities. // Elke frame syncen we transform van de HTML-card met body.position + body.angle. import Matter from 'https://esm.sh/[email protected]'; ;(() => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const { Engine, Runner, Bodies, Composite, Mouse, MouseConstraint } = Matter; const host = document.querySelector('[data-physics-host]'); if (!host) return; const init = () => { const W = host.clientWidth, H = host.clientHeight; if (!W || !H) { requestAnimationFrame(init); return; } const engine = Engine.create(); engine.gravity.y = 1; const cards = Array.from(host.querySelectorAll('[data-physics-card]')); const bodies = cards.map((el, i) => { const w = el.offsetWidth || 240; const h = el.offsetHeight || 110; const x = 100 + (i * (W - 200)) / Math.max(cards.length - 1, 1); const y = -120 - i * 90; const body = Bodies.rectangle(x, y, w, h, { restitution: 0.35, friction: 0.08, frictionAir: 0.012, angle: (Math.random() - 0.5) * 0.6 }); return { el, body }; }); Composite.add(engine.world, [ Bodies.rectangle(W/2, H+30, W*2, 60, { isStatic: true }), Bodies.rectangle(-30, H/2, 60, H*2, { isStatic: true }), Bodies.rectangle(W+30, H/2, 60, H*2, { isStatic: true }), ...bodies.map(b => b.body), ]); Composite.add(engine.world, MouseConstraint.create(engine, { mouse: Mouse.create(host), constraint: { stiffness: 0.2, render: { visible: false } } })); Runner.run(Runner.create(), engine); (function tick() { bodies.forEach(({ el, body }) => { el.style.transform = 'translate3d(' + body.position.x + 'px,' + body.position.y + 'px,0) ' + 'translate(-50%,-50%) rotate(' + body.angle + 'rad)'; }); requestAnimationFrame(tick); })(); }; // IntersectionObserver — alleen initialiseren als host zichtbaar is, // anders zijn clientWidth/Height onbetrouwbaar. new IntersectionObserver((entries, obs) => { if (entries.some(e => e.isIntersecting)) { obs.disconnect(); init(); } }, { threshold: 0.05 }).observe(host); })();
<!-- Skeleton: physics-cards (laag B) — ECHTE content-cards, niet leeg -->
<div data-physics-host class="physics-host">
<article data-physics-card class="p-card">
<span class="p-card__tag">01 / Discovery</span>
<h3 class="p-card__title">Workshop kickoff</h3>
<p class="p-card__body">Stakeholder mapping, research, scope.</p>
</article>
<article data-physics-card class="p-card">
<span class="p-card__tag">02 / Audit</span>
<h3 class="p-card__title">Brand & funnel audit</h3>
<p class="p-card__body">Gap-analyse op huidige performance.</p>
</article>
<article data-physics-card class="p-card">
<span class="p-card__tag">03 / Strategy</span>
<h3 class="p-card__title">Positionering</h3>
<p class="p-card__body">Archetype, claim, doelgroep-fit.</p>
</article>
<article data-physics-card class="p-card">
<span class="p-card__tag">04 / Build</span>
<h3 class="p-card__title">Site & systemen</h3>
<p class="p-card__body">Astro, CMS, integraties, tests.</p>
</article>
<article data-physics-card class="p-card">
<span class="p-card__tag">05 / Launch</span>
<h3 class="p-card__title">Go-live + warm-up</h3>
<p class="p-card__body">DNS, deliverability, GA4.</p>
</article>
<article data-physics-card class="p-card">
<span class="p-card__tag">06 / Scale</span>
<h3 class="p-card__title">Optimalisatie-loop</h3>
<p class="p-card__body">A/B, content, paid scaling.</p>
</article>
</div> /* Styling-template: BRUTALIST (laag C) */
.physics-host {
position: relative;
height: 70vh;
background: #FFFFFF;
border: 2px solid #0A0A0A;
overflow: hidden;
}
.p-card {
position: absolute;
left: 0; top: 0;
width: 240px;
padding: 18px 20px;
background: #FFFFFF;
color: #0A0A0A;
border: 2px solid #0A0A0A;
border-radius: 0;
box-shadow: 8px 8px 0 0 #0A0A0A, 16px 16px 0 0 #FF4A1C;
font-family: 'Archivo', sans-serif;
will-change: transform;
}
.p-card:nth-child(3n) { background: #FF4A1C; color: #FFFFFF; box-shadow: 8px 8px 0 0 #0A0A0A; }
.p-card:nth-child(5n) { background: #0A0A0A; color: #FFFFFF; box-shadow: 8px 8px 0 0 #FF4A1C; }
.p-card__tag {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
letter-spacing: .14em;
text-transform: uppercase;
display: block;
margin-bottom: 10px;
}
.p-card__title {
font-family: 'Archivo', sans-serif;
font-weight: 700;
font-size: 18px;
margin: 0 0 6px;
text-transform: uppercase;
letter-spacing: -.01em;
}
.p-card__body {
font-size: 12px;
line-height: 1.4;
margin: 0;
opacity: .85;
}