All components

Interactive Divs with Physics

JavaScript animation component · Published 2026-07-27 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Twelve grayscale polaroid-style image cards float over a full-screen p5.js canvas inside a zero-gravity Matter.js world, each DOM div repositioned every frame from its physics body. Moving the mouse near a card fires a random Matter.js Body.applyForce, so nearby cards scatter, drift with air friction and restitution, and bounce off the invisible viewport walls.

How it's built

Categoryinteractive
Techmatter-js
Complexitypage
Performance costheavy
Mobile-safedesktop-first

physics matter-js p5js mouse-interaction floating-cards image-scatter interactive cinematic

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Interactive Divs with Physics — floating grayscale cards that scatter from the cursor

Goal

Build a full-screen, black, zero-gravity playground where twelve grayscale polaroid-style image cards drift over the whole viewport. Each card is a real HTML <div> whose position and rotation are driven, every frame, by a Matter.js rigid body living in a gravity-free physics world. A big centered word sits behind them. The star effect: moving the mouse near a card fires a random impulse into it, so nearby cards shoot away, slowly coast to a stop under air friction, gently bounce off the invisible viewport walls, and keep floating. It reads like a slow-motion swarm of photo prints you can bat around with the cursor.

Tech

Vanilla HTML/CSS/JS. Two libraries with two different loading mechanisms (match this exactly — the sketch depends on p5 running in "global mode"):

  1. p5.js v1.4.0 as a classic CDN <script> in <head> (NOT an npm import). This runs p5 in global mode: p5 auto-initializes on window load and attaches globals like createCanvas, random, dist, background, width, height, mouseX, mouseY, and calls your global setup() / draw() / mouseMoved() functions.

``html <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> ``

  1. matter-js as an npm ESM import in your module script:

``js import Matter from "matter-js"; const { Engine, World, Bodies, Body } = Matter; ``

No GSAP, no ScrollTrigger, no Lenis, no framework. The page does not scroll.

Global-mode bridge (critical): because your code is an ES module, setup/draw/mouseMoved are module-scoped and p5 can't see them. At the very bottom of the module, expose them on window:

window.setup = setup;
window.draw = draw;
window.mouseMoved = mouseMoved;

Layout / HTML

Minimal. p5 injects its own <canvas> and the JS creates the card divs at runtime.

<body>
  <div class="header"><h1>Motionprompts</h1></div>
  <script type="module" src="./script.js"></script>
</body>
  • One .header with an <h1> (fictional brand text Motionprompts), absolutely centered — it sits *behind* the floating cards (the p5 canvas and the .item divs are appended to <body> after it and paint on top).
  • No card markup in HTML: the twelve <div class="item"> elements (each containing one <img>) are created in JS and appended to document.body.

Styling

Reset and full-bleed stage:

  • * { margin:0; padding:0; box-sizing:border-box; }
  • html, body { width:100vw; height:100vh; overflow:hidden; background:#000; } — the page is exactly one viewport, no scroll, pure black behind everything.

Header (centered background word):

  • .header { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
  • .header h1 { font-family:"Circular Std", sans-serif; font-size:12vw; font-weight:500; letter-spacing:-0.05em; line-height:175%; color:#fff; text-align:center; } — a very large, tight, geometric-sans word in white. Any clean geometric sans works as fallback.

The cards (.item — a white polaroid frame):

  • .item { position:absolute; width:200px; height:225px; padding:0.5em 0.5em 4em 0.5em; overflow:hidden; background:#fff; } — a 200×225px white card with an asymmetric pad (thin on top/sides, 4em thick at the bottom) so the photo sits in the upper area and leaves a polaroid-style white chin.
  • .item img { width:100%; height:100%; object-fit:cover; filter:grayscale(100%); } — the photo fills the padded content box, cropped, and rendered fully grayscale.

JS sets each card's inline left, top, and transform: rotate(<rad>) every frame (see below).

Physics + render effect (be exhaustive — this is the whole component)

Engine & world
let engine = Engine.create();
engine.world.gravity.y = 0;   // ZERO gravity — cards float, they never fall

Keep module-level state: engine, items = [], lastMouseX = -1, lastMouseY = -1.

setup() (runs once)
  1. createCanvas(window.innerWidth, window.innerHeight); — a p5 canvas the full size of the window.
  2. engine = Engine.create(); then engine.world.gravity.y = 0;.
  3. addBoundaries(); (below).
  4. Spawn 12 cards in a loop for (let i = 0; i < 12; i++):
  5. x = random(100, width - 100), y = random(100, height - 100) — random start inside a 100px inset.
  6. items.push(new Item(x, y, "img" + (i + 1) + ".jpg")); — image sources img1.jpg … img12.jpg.
addBoundaries() — four invisible static walls

Four static rectangles of thickness = 50, positioned so their inner edge lines up with the viewport edges (bodies sit just *outside* the visible area). Add all four with World.add(engine.world, [...]):

  • Top: Bodies.rectangle(width/2, -thickness/2, width, thickness, { isStatic:true })
  • Bottom: Bodies.rectangle(width/2, height + thickness/2, width, thickness, { isStatic:true })
  • Left: Bodies.rectangle(-thickness/2, height/2, thickness, height, { isStatic:true })
  • Right: Bodies.rectangle(width + thickness/2, height/2, thickness, height, { isStatic:true })

These keep the cards contained; cards bounce off them (see restitution).

Item class

Constructor (x, y, imagePath):

  • Physics body options: { frictionAir: 0.075, restitution: 0.25, density: 0.002, angle: Math.random() * Math.PI * 2 }.
  • frictionAir: 0.075 — heavy air drag, so impulses decay to a stop within ~1s (the slow, floaty coast).
  • restitution: 0.25 — soft, low-energy bounces off walls and each other.
  • density: 0.002 — light bodies (mass = density × area = 0.002 × 100 × 200 = 40) so the impulse below produces a big, visible kick.
  • angle — each card starts at a random rotation (0–2π).
  • Body: this.body = Bodies.rectangle(x, y, 100, 200, options); — a 100×200 rectangle body (note: smaller than the 200×225 visual card; this is intentional/original). World.add(engine.world, this.body);.
  • DOM: create <div class="item">, position it (see offset math), create an <img> with src = imagePath, append the img to the div, append the div to document.body.

Position offset (used in constructor and every frame in update()):

this.div.style.left = (this.body.position.x - 50) + "px";
this.div.style.top  = (this.body.position.y - 100) + "px";

The div's top-left is offset −50px x / −100px y from the body center (half of the 100×200 *body* size, not the card size — keep these exact numbers).

update() (called every frame):

this.div.style.left = (this.body.position.x - 50) + "px";
this.div.style.top  = (this.body.position.y - 100) + "px";
this.div.style.transform = "rotate(" + this.body.angle + "rad)";

So each card's screen position and rotation mirror its physics body exactly, in radians.

draw() — the p5 render loop (~60fps)
function draw() {
  background("black");        // repaint canvas black each frame
  Engine.update(engine);      // step the physics simulation one tick
  items.forEach((item) => item.update());  // sync every card div to its body
}

The canvas itself only ever shows solid black; all the visible motion is the DOM .item divs being repositioned on top of it. There is no noLoop() — the loop runs continuously so bodies keep integrating (drifting, decaying, settling) even without input.

mouseMoved() — the interaction (the star moment)

Throttle by how far the mouse has moved, then push nearby cards:

function mouseMoved() {
  if (dist(mouseX, mouseY, lastMouseX, lastMouseY) > 10) {
    lastMouseX = mouseX;
    items.forEach((item) => {
      if (dist(mouseX, mouseY, item.body.position.x, item.body.position.y) < 150) {
        const forceMagnitude = 3;
        Body.applyForce(
          item.body,
          { x: item.body.position.x, y: item.body.position.y },   // apply at the body's own center → pure linear push, no torque
          { x: random(-forceMagnitude, forceMagnitude),
            y: random(-forceMagnitude, forceMagnitude) }           // random 2D impulse in [-3, 3] on each axis
        );
      }
    });
  }
}

Behavior to reproduce:

  • Only reacts once the cursor has traveled >10px since the last sampled point (lastMouseX/lastMouseY seeded at -1, so it fires as soon as the mouse enters the page).
  • For every card whose body center is within 150px of the cursor, apply a random force vector, each component uniformly in [-3, 3], at the card's own center of mass (so it's a straight shove, no spin from the force — any rotation you see is incidental collision response).
  • With frictionAir 0.075 the shoved cards fan out fast, then glide to rest; restitution 0.25 gives them a gentle bounce if they reach a wall or clip a neighbor.

Assets / images

12 editorial photographs, img1.jpg … img12.jpg, one per card. Each is displayed cropped (object-fit:cover) into the polaroid's ~184×153px content window and forced to grayscale by CSS, so any source palette is fine. Aim for a cohesive, moody, cinematic/editorial set — e.g. atmospheric portraits, sci-fi/astronaut scenes, minimalist product shots, monochrome cityscapes — anything with strong tonal contrast reads well once desaturated. No logos or real brand marks. (If fewer than 12 images are available, repeat them in order.)

Behavior notes

  • Desktop / pointer-driven, page-level component. The whole viewport is the stage; nothing scrolls (overflow:hidden, page is exactly 100vw × 100vh). Not mobile-safe (relies on mousemove).
  • Continuous simulation, no autoplay animation: cards start at random positions and rotations and simply sit still (zero gravity, no initial velocity) until the cursor disturbs them; then they scatter and re-settle. Cards can overlap and gently jostle via collisions.
  • Canvas is sized to the window at load; there is no resize handler in the original (bodies/walls keep their initial dimensions if the window is resized).
  • No reduced-motion handling and no console errors expected. Heavy-ish (12 DOM nodes synced to a 60fps physics loop) but smooth on desktop.