1. Mechanisme — kopieer 1-op-1, geen styling-keuzes
// Mechanisme: Matter.js physics op DOM-cards (laag A)
// Zelfde core als andere physics-varianten. Verschil zit in styling +
// gravity (zachter voor minimal-feel).
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 = 0.7;
const cards = Array.from(host.querySelectorAll('[data-physics-card]'));
const bodies = cards.map((el, i) => {
const w = el.offsetWidth, h = el.offsetHeight;
const body = Bodies.rectangle(
80 + (i * (W - 160)) / Math.max(cards.length - 1, 1),
-80 - i * 50,
w, h,
{ restitution: 0.25, friction: 0.12, frictionAir: 0.02 }
);
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.15, 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);
})();
};
new IntersectionObserver((es, o) => {
if (es.some(e => e.isIntersecting)) { o.disconnect(); init(); }
}, { threshold: 0.05 }).observe(host);
})(); 3. Styling-template — verplicht eigen invulling per merk
/* Styling-template: MINIMAL (laag C) */
.physics-host {
position: relative;
height: 70vh;
background: #F4F1EB;
border-top: 1px solid rgba(10,10,10,.12);
border-bottom: 1px solid rgba(10,10,10,.12);
overflow: hidden;
}
.m-card {
position: absolute;
left: 0; top: 0;
width: 110px;
height: 110px;
background: #FFFFFF;
color: #0A0A0A;
border: none;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 6px;
font-family: 'Inter', 'Archivo', sans-serif;
will-change: transform;
}
.m-card::before {
content: '';
position: absolute;
top: 12px; left: 12px; right: 12px;
height: 1px;
background: rgba(10,10,10,.18);
}
.m-card__num {
font-size: 28px;
font-weight: 300;
letter-spacing: -.02em;
color: #0A0A0A;
}
.m-card__label {
font-size: 10px;
letter-spacing: .12em;
text-transform: uppercase;
color: rgba(10,10,10,.5);
}