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/matter-js@0.19.0';
;(() => {
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;
}