← Blurr Motion content-physics-cards-minimal
Categorie content Tier 2 Techniek #31 Deps matter-js
01Discovery
02Audit
03Strategy
04Build
05Launch
06Scale
07Iterate
08Review
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);
})();
2. Skeleton — DOM + class-namen, mag herschikken
<!-- Skeleton: physics-cards minimal (laag B) -->
<!-- Zacht-grijze cards met cijfer + label, géén schaduwen, géén accent-kleur. -->
<div data-physics-host class="physics-host">
  <article data-physics-card class="m-card"><span class="m-card__num">01</span><span class="m-card__label">Discovery</span></article>
  <article data-physics-card class="m-card"><span class="m-card__num">02</span><span class="m-card__label">Audit</span></article>
  <article data-physics-card class="m-card"><span class="m-card__num">03</span><span class="m-card__label">Strategy</span></article>
  <article data-physics-card class="m-card"><span class="m-card__num">04</span><span class="m-card__label">Build</span></article>
  <article data-physics-card class="m-card"><span class="m-card__num">05</span><span class="m-card__label">Launch</span></article>
  <article data-physics-card class="m-card"><span class="m-card__num">06</span><span class="m-card__label">Scale</span></article>
  <article data-physics-card class="m-card"><span class="m-card__num">07</span><span class="m-card__label">Iterate</span></article>
  <article data-physics-card class="m-card"><span class="m-card__num">08</span><span class="m-card__label">Review</span></article>
</div>
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);
}