← Blurr Motion content-physics-cards-brutalist
Categorie content Tier 2 Techniek #31 Deps matter-js
01 / Discovery

Workshop kickoff

Stakeholder mapping, research, scope.

02 / Audit

Brand & funnel audit

Gap-analyse op huidige performance.

03 / Strategy

Positionering

Archetype, claim, doelgroep-fit.

04 / Build

Site & systemen

Astro, CMS, integraties, tests.

05 / Launch

Go-live + warm-up

DNS, deliverability, GA4.

06 / Scale

Optimalisatie-loop

A/B, content, paid scaling.

1. Mechanisme — kopieer 1-op-1, geen styling-keuzes
// 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);
})();
2. Skeleton — DOM + class-namen, mag herschikken
<!-- 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>
3. Styling-template — verplicht eigen invulling per merk
/* 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;
}